HTTP/1 Parser

This commit is contained in:
HoneyryderChuck 2018-11-14 14:40:28 +00:00
parent 626c77f758
commit 446d561ed5
15 changed files with 771 additions and 66 deletions

View File

@ -16,6 +16,9 @@ Metrics/ModuleLength:
Metrics/BlockLength:
Max: 100
Metrics/BlockNesting:
Enabled: False
#Naming/MethodName:
# TODO: remove this if min supported version of ruby is 2.3

View File

@ -5,5 +5,5 @@ SimpleCov.start do
add_filter "/lib/httpx/extensions.rb"
add_filter "/lib/httpx/loggable.rb"
coverage_dir "www/coverage"
minimum_coverage 85
minimum_coverage 80
end

View File

@ -13,6 +13,7 @@ gem "simplecov", require: false
platform :mri do
gem "brotli", require: false
gem "pry-byebug", require: false
gem "benchmark-ips", require: false
end
# gem "guard-rspec", :require => false
# gem "nokogiri", :require => false
@ -21,3 +22,4 @@ gem "pry", :require => false
gem "minitest", require: false
gem "minitest-proveit", require: false
gem "oga", require: false
gem "http_parser.rb", require: false

View File

@ -1,6 +1,6 @@
version: '3'
services:
httpx:
image: jruby:9.1.16-alpine
image: jruby:9.2.3-alpine
environment:
- JRUBY_OPTS=--debug

View File

@ -22,7 +22,6 @@ Gem::Specification.new do |gem|
gem.add_runtime_dependency "http-2", ">= 0.9.0"
gem.add_runtime_dependency "http-form_data", ">= 2.0.0", "< 3"
gem.add_runtime_dependency "http_parser.rb", ">= 0.6.0"
gem.add_development_dependency "http-cookie", "~> 1.0"
end

View File

@ -204,6 +204,7 @@ module HTTPX
return if siz.zero?
log { "READ: #{siz} bytes..." }
parser << @read_buffer.to_s
return if @state == :closing || @state == :closed
end
end
@ -219,6 +220,7 @@ module HTTPX
end
log { "WRITE: #{siz} bytes..." }
return if siz.zero?
return if @state == :closing || @state == :closed
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require "http_parser"
require "httpx/parser/http1"
module HTTPX
class Channel::HTTP1
@ -12,18 +12,15 @@ module HTTPX
def initialize(buffer, options)
@options = Options.new(options)
@max_concurrent_requests = @options.max_concurrent_requests
@parser = HTTP::Parser.new(self)
@parser.header_value_type = :arrays
@parser = Parser::HTTP1.new(self)
@buffer = buffer
@version = [1, 1]
@pending = []
@requests = []
@has_response = false
end
def reset
@parser.reset!
@has_response = false
end
def close
@ -39,7 +36,6 @@ module HTTPX
def <<(data)
@parser << data
dispatch if @has_response
end
def send(request, **)
@ -69,69 +65,59 @@ module HTTPX
#
# must be public methods, or else they won't be reachable
def on_message_begin
def on_start
log(level: 2) { "parsing begins" }
end
def on_headers_complete(h)
return on_trailer_headers_complete(h) if @parser_trailers
# Wait for fix: https://github.com/tmm1/http_parser.rb/issues/52
# callback is called 2 times when chunked
request = @requests.first
return if request.response
def on_headers(h)
@request = @requests.first
return if @request.response
log(level: 2) { "headers received" }
headers = @options.headers_class.new(h)
response = @options.response_class.new(@requests.last,
response = @options.response_class.new(@request,
@parser.status_code,
@parser.http_version.join("."),
headers, @options)
log(color: :yellow) { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version.join(".")}" }
log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{v}" }.join("\n") }
request.response = response
@has_response = true if response.complete?
@request.response = response
on_complete if response.complete?
end
def on_body(chunk)
log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
log(level: 2, color: :green) { "-> #{chunk.inspect}" }
response = @requests.first.response
response << chunk
@has_response = response.complete?
end
def on_message_complete
log(level: 2) { "parsing complete" }
request = @requests.first
response = request.response
if !@parser_trailers && response.headers.key?("trailer")
@parser_trailers = true
# this is needed, because the parser can't accept further headers.
# we need to reset it and artificially move it to receive headers state,
# hence the bogus headline
#
@parser.reset!
@parser << "#{request.verb.to_s.upcase} #{request.path} HTTP/#{response.version}#{CRLF}"
else
@has_response = true
end
end
def on_trailer_headers_complete(h)
response = @requests.first.response
def on_trailers(h)
return unless @request
response = @request.response
log(level: 2) { "trailer headers received" }
log(color: :yellow) { h.each.map { |f, v| "-> HEADER: #{f}: #{v}" }.join("\n") }
response.merge_headers(h)
end
def dispatch
request = @requests.first
return handle(request) if request.expects?
def on_data(chunk)
return unless @request
log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
log(level: 2, color: :green) { "-> #{chunk.inspect}" }
response = @request.response
response << chunk
end
def on_complete
return unless @request
log(level: 2) { "parsing complete" }
dispatch
end
def dispatch
if @request.expects?
reset
return handle(@request)
end
request = @request
@request = nil
@requests.shift
response = request.response
emit(:response, request, response)
@ -178,7 +164,6 @@ module HTTPX
end
def handle(request)
@has_response = false
set_request_headers(request)
catch(:buffer_full) do
request.transition(:headers)

View File

@ -47,7 +47,6 @@ module HTTPX
def on_promise(_, stream)
log(level: 2, label: "#{stream.id}: ") { "refusing stream!" }
stream.refuse
# TODO: policy for handling promises
end
def fetch_response(request)

171
lib/httpx/parser/http1.rb Normal file
View File

@ -0,0 +1,171 @@
# frozen_string_literal: true
module HTTPX
module Parser
Error = Class.new(Error)
class HTTP1
VERSIONS = %w[0.9 1.0 1.1].freeze
attr_reader :status_code, :http_version, :headers
def initialize(observer, header_separator: ":")
@observer = observer
@state = :idle
@header_separator = header_separator
@buffer = "".b
@headers = {}
end
def <<(chunk)
@buffer << chunk
parse
end
def reset!
@state = :idle
@headers.clear
@content_length = nil
@_has_trailers = nil
end
def upgrade?
@upgrade
end
def upgrade_data
@buffer
end
private
def parse
state = @state
case @state
when :idle
parse_headline
when :headers
parse_headers
when :trailers
parse_headers
when :data
parse_data
end
parse if !@buffer.empty? && state != @state
end
def parse_headline
idx = @buffer.index("\n")
return unless idx
(m = %r{\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?}in.match(@buffer)) ||
raise(Error, "wrong head line format")
version, code, _ = m.captures
raise(Error, "unsupported HTTP version (HTTP/#{version})") unless VERSIONS.include?(version)
@http_version = version.split(".").map(&:to_i)
@status_code = code.to_i
raise(Error, "wrong status code (#{@status_code})") unless (100..599).cover?(@status_code)
@buffer.slice!(0, idx + 1)
nextstate(:headers)
end
def parse_headers
headers = @headers
while (idx = @buffer.index("\n"))
line = @buffer.slice!(0, idx + 1).sub(/\s+\z/, "")
if line.empty?
case @state
when :headers
prepare_data(headers)
@observer.on_headers(headers)
return unless @state == :headers
# state might have been reset
# in the :headers callback
nextstate(:data)
headers.clear
when :trailers
@observer.on_trailers(headers)
headers.clear
nextstate(:complete)
else
raise Error, "wrong header format"
end
return
end
separator_index = line.index(@header_separator)
raise Error, "wrong header format" unless separator_index
key = line[0..separator_index - 1]
raise Error, "wrong header format" if key.start_with?("\s", "\t")
key.strip!
value = line[separator_index + 1..-1]
value.strip!
raise Error, "wrong header format" if value.nil?
(headers[key.downcase] ||= []) << value
end
end
def parse_data
if @buffer.respond_to?(:each)
@buffer.each do |chunk|
@observer.on_data(chunk)
end
elsif @content_length
data = @buffer.slice!(0, @content_length)
@content_length -= data.bytesize
@observer.on_data(data)
data.clear
else
@observer.on_data(@buffer)
@buffer.clear
end
return unless no_more_data?
@buffer = @buffer.to_s
if @_has_trailers
nextstate(:trailers)
else
nextstate(:complete)
end
end
def prepare_data(headers)
@upgrade = headers.key?("upgrade")
@_has_trailers = headers.key?("trailer")
if (tr_encodings = headers["transfer-encoding"])
tr_encodings.reverse_each do |tr_encoding|
tr_encoding.split(/ *, */).each do |encoding|
case encoding
when "chunked"
@buffer = Transcoder::Chunker::Decoder.new(@buffer, @_has_trailers)
end
end
end
else
@content_length = headers["content-length"][0].to_i if headers.key?("content-length")
end
end
def no_more_data?
if @content_length
@content_length <= 0
elsif @buffer.respond_to?(:finished?)
@buffer.finished?
else
false
end
end
def nextstate(state)
@state = state
case state
when :headers
@observer.on_start
when :complete
@observer.on_complete
reset!
nextstate(:idle) unless @buffer.empty?
end
end
end
end
end

View File

@ -55,6 +55,7 @@ module HTTPX
end
def build_channel(proxy, options)
return super if proxy.is_a?(URI::Generic)
channel = build_proxy_channel(proxy, **options)
set_channel_callbacks(channel, options)
channel

View File

@ -41,11 +41,7 @@ module HTTPX
def bodyless?
@request.verb == :head ||
@status < 200 ||
@status == 201 ||
@status == 204 ||
@status == 205 ||
@status == 304
no_data?
end
def content_type
@ -65,6 +61,19 @@ module HTTPX
raise HTTPError, self
end
private
def no_data?
@status < 200 ||
@status == 204 ||
@status == 205 ||
@status == 304 || begin
content_length = @headers["content-length"]
return false if content_length.nil?
content_length == "0"
end
end
class Body
def initialize(response, threshold_size:, window_size: 1 << 14)
@response = response
@ -170,7 +179,7 @@ module HTTPX
when :memory
if @length > @threshold_size
aux = @buffer
@buffer = Tempfile.new("palanca", encoding: @encoding, mode: File::RDWR)
@buffer = Tempfile.new("httpx", encoding: @encoding, mode: File::RDWR)
aux.rewind
::IO.copy_stream(aux, @buffer)
# TODO: remove this if/when minor ruby is 2.3

View File

@ -4,15 +4,14 @@ require "forwardable"
module HTTPX::Transcoder
module Chunker
module_function
Error = Class.new(HTTPX::Error)
CRLF = "\r\n".b
class Encoder
extend Forwardable
def_delegator :@raw, :readpartial
CRLF = "\r\n"
def initialize(body)
@raw = body
end
@ -30,6 +29,80 @@ module HTTPX::Transcoder
end
end
class Decoder
extend Forwardable
def_delegator :@buffer, :empty?
def_delegator :@buffer, :<<
def_delegator :@buffer, :clear
def initialize(buffer, trailers = false)
@buffer = buffer
@chunk_length = nil
@chunk_buffer = "".b
@finished = false
@state = :length
@trailers = trailers
end
def to_s
@buffer
end
def each
loop do
case @state
when :length
index = @buffer.index(CRLF)
return unless index && index.positive?
# Read hex-length
hexlen = @buffer.slice!(0, index)
hexlen[/\h/] || raise(Error, "wrong chunk size line: #{hexlen}")
@chunk_length = hexlen.hex
# check if is last chunk
@finished = @chunk_length.zero?
nextstate(:crlf)
when :crlf
crlf_size = @finished && !@trailers ? 4 : 2
# consume CRLF
return if @buffer.bytesize < crlf_size
raise Error, "wrong chunked encoding format" unless @buffer.start_with?(CRLF * (crlf_size / 2))
@buffer.slice!(0, crlf_size)
if @chunk_length.nil?
nextstate(:length)
else
return if @finished
nextstate(:data)
end
when :data
@chunk_buffer << (slice = @buffer.slice!(0, @chunk_length))
@chunk_length -= slice.bytesize
if @chunk_length.zero?
yield @chunk_buffer unless @chunk_buffer.empty?
@chunk_buffer.clear
@chunk_length = nil
nextstate(:crlf)
end
end
break if @buffer.empty?
end
end
def finished?
@finished
end
private
def nextstate(state)
@state = state
end
end
module_function
def encode(chunks)
Encoder.new(chunks)
end

58
test/parser_test.rb Normal file
View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
require_relative "test_helper"
class HTTP1ParserTest < Minitest::Test
include HTTPX
class RequestObserver
attr_reader :headers, :body
def initialize
@headers = {}
@body = "".b
end
def on_headers(h)
@headers.merge!(h)
end
def on_data(data)
@body << data
end
def on_trailers(*); end
def on_start; end
def on_complete; end
end
JSON.parse(File.read(File.expand_path("support/responses.json", __dir__))).each do |res_json|
res_json["headers"] ||= {}
define_method "test_parse_response_#{res_json["name"]}" do
observer = RequestObserver.new
parser = Parser::HTTP1.new(observer)
parser << res_json["raw"].b
if res_json.key?("upgrade") && (res_json["upgrade"] != 0)
expect(@parser.upgrade?).to be true
expect(@parser.upgrade_data).to eq(res_json["upgrade"])
end
assert parser.http_version[0] == res_json["http_major"]
assert parser.http_version[1] == res_json["http_minor"]
assert parser.status_code == res_json["status_code"]
assert observer.headers.size == res_json["num_headers"]
res_json["headers"].each do |field, value|
assert value == observer.headers[field.downcase].join("; ")
end
assert observer.body == res_json["body"]
assert observer.body.size == res_json["body_size"] if res_json["body_size"]
end
end
end

View File

@ -1,8 +1,16 @@
#!/bin/sh
apk --update add g++ make git bash
RUBY_PLATFORM=`ruby -e 'puts RUBY_PLATFORM'`
if [[ "$RUBY_PLATFORM" = "java" ]]; then
apk --update add git bash
else
apk --update add g++ make git bash
fi
export PATH=$GEM_HOME/bin:$BUNDLE_PATH/gems/bin:$PATH
mkdir -p "$GEM_HOME" && chmod 777 "$GEM_HOME"
gem install bundler -v="1.16.1" --no-doc --conservative
gem install bundler -v="1.17.0" --no-doc --conservative
cd /home && bundle install --quiet --jobs 4 && \
bundle exec rake test:ci

395
test/support/responses.json Normal file
View File

@ -0,0 +1,395 @@
[
{
"name": "google 301",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 301 Moved Permanently\r\nLocation: http://www.google.com/\r\nContent-Type: text/html; charset=UTF-8\r\nDate: Sun, 26 Apr 2009 11:11:49 GMT\r\nExpires: Tue, 26 May 2009 11:11:49 GMT\r\nX-$PrototypeBI-Version: 1.6.0.3\r\nCache-Control: public, max-age=2592000\r\nServer: gws\r\nContent-Length: 219 \r\n\r\n<HTML><HEAD><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n<TITLE>301 Moved</TITLE></HEAD><BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF=\"http://www.google.com/\">here</A>.\r\n</BODY></HTML>\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 301,
"status": "Moved Permanently",
"num_headers": 8,
"headers": {
"Location": "http://www.google.com/",
"Content-Type": "text/html; charset=UTF-8",
"Date": "Sun, 26 Apr 2009 11:11:49 GMT",
"Expires": "Tue, 26 May 2009 11:11:49 GMT",
"X-$PrototypeBI-Version": "1.6.0.3",
"Cache-Control": "public, max-age=2592000",
"Server": "gws",
"Content-Length": "219"
},
"body": "<HTML><HEAD><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n<TITLE>301 Moved</TITLE></HEAD><BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF=\"http://www.google.com/\">here</A>.\r\n</BODY></HTML>\r\n",
"strict": true
},
{
"name": "no content-length response",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nDate: Tue, 04 Aug 2009 07:59:32 GMT\r\nServer: Apache\r\nX-Powered-By: Servlet/2.5 JSP/2.1\r\nContent-Type: text/xml; charset=utf-8\r\nConnection: close\r\n\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n <SOAP-ENV:Body>\n <SOAP-ENV:Fault>\n <faultcode>SOAP-ENV:Client</faultcode>\n <faultstring>Client Error</faultstring>\n </SOAP-ENV:Fault>\n </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 5,
"headers": {
"Date": "Tue, 04 Aug 2009 07:59:32 GMT",
"Server": "Apache",
"X-Powered-By": "Servlet/2.5 JSP/2.1",
"Content-Type": "text/xml; charset=utf-8",
"Connection": "close"
},
"body": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n <SOAP-ENV:Body>\n <SOAP-ENV:Fault>\n <faultcode>SOAP-ENV:Client</faultcode>\n <faultstring>Client Error</faultstring>\n </SOAP-ENV:Fault>\n </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>",
"strict": true
},
{
"name": "404 no headers no body",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 404 Not Found\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 1,
"status_code": 404,
"status": "Not Found",
"num_headers": 0,
"headers": {
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "301 no response phrase",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 301\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 1,
"status_code": 301,
"status": "",
"num_headers": 0,
"headers": {
},
"body": "",
"strict": true
},
{
"name": "200 trailing space on chunked body",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n25 \r\nThis is the data in the first chunk\r\n\r\n1C\r\nand this is the second one\r\n\r\n0 \r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 2,
"headers": {
"Content-Type": "text/plain",
"Transfer-Encoding": "chunked"
},
"body_size": 65,
"body": "This is the data in the first chunk\r\nand this is the second one\r\n",
"strict": true
},
{
"name": "no carriage ret",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\nConnection: close\n\nthese headers are from http://news.ycombinator.com/",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 2,
"headers": {
"Content-Type": "text/html; charset=utf-8",
"Connection": "close"
},
"body": "these headers are from http://news.ycombinator.com/",
"strict": true
},
{
"name": "proxy connection",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: 11\r\nProxy-Connection: close\r\nDate: Thu, 31 Dec 2009 20:55:48 +0000\r\n\r\nhello world",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 4,
"headers": {
"Content-Type": "text/html; charset=UTF-8",
"Content-Length": "11",
"Proxy-Connection": "close",
"Date": "Thu, 31 Dec 2009 20:55:48 +0000"
},
"body": "hello world",
"strict": true
},
{
"name": "underscore header key",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nServer: DCLK-AdSvr\r\nContent-Type: text/xml\r\nContent-Length: 0\r\nDCLK_imp: v7;x;114750856;0-0;0;17820020;0/0;21603567/21621457/1;;~okv=;dcmt=text/xml;;~cs=o\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 4,
"headers": {
"Server": "DCLK-AdSvr",
"Content-Type": "text/xml",
"Content-Length": "0",
"DCLK_imp": "v7;x;114750856;0-0;0;17820020;0/0;21603567/21621457/1;;~okv=;dcmt=text/xml;;~cs=o"
},
"body": "",
"strict": true
},
{
"name": "bonjourmadame.fr",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.0 301 Moved Permanently\r\nDate: Thu, 03 Jun 2010 09:56:32 GMT\r\nServer: Apache/2.2.3 (Red Hat)\r\nCache-Control: public\r\nPragma: \r\nLocation: http://www.bonjourmadame.fr/\r\nVary: Accept-Encoding\r\nContent-Length: 0\r\nContent-Type: text/html; charset=UTF-8\r\nConnection: keep-alive\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 0,
"status_code": 301,
"status": "Moved Permanently",
"num_headers": 9,
"headers": {
"Date": "Thu, 03 Jun 2010 09:56:32 GMT",
"Server": "Apache/2.2.3 (Red Hat)",
"Cache-Control": "public",
"Pragma": "",
"Location": "http://www.bonjourmadame.fr/",
"Vary": "Accept-Encoding",
"Content-Length": "0",
"Content-Type": "text/html; charset=UTF-8",
"Connection": "keep-alive"
},
"body": "",
"strict": true
},
{
"name": "field underscore",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nDate: Tue, 28 Sep 2010 01:14:13 GMT\r\nServer: Apache\r\nCache-Control: no-cache, must-revalidate\r\nExpires: Mon, 26 Jul 1997 05:00:00 GMT\r\n.et-Cookie: PlaxoCS=1274804622353690521; path=/; domain=.plaxo.com\r\nVary: Accept-Encoding\r\n_eep-Alive: timeout=45\r\n_onnection: Keep-Alive\r\nTransfer-Encoding: chunked\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n0\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 11,
"headers": {
"Date": "Tue, 28 Sep 2010 01:14:13 GMT",
"Server": "Apache",
"Cache-Control": "no-cache, must-revalidate",
"Expires": "Mon, 26 Jul 1997 05:00:00 GMT",
".et-Cookie": "PlaxoCS=1274804622353690521; path=/; domain=.plaxo.com",
"Vary": "Accept-Encoding",
"_eep-Alive": "timeout=45",
"_onnection": "Keep-Alive",
"Transfer-Encoding": "chunked",
"Content-Type": "text/html",
"Connection": "close"
},
"body": "",
"strict": true
},
{
"name": "non-ASCII in status line",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 500 Oriëntatieprobleem\r\nDate: Fri, 5 Nov 2010 23:07:12 GMT+2\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 500,
"status": "Oriëntatieprobleem",
"num_headers": 3,
"headers": {
"Date": "Fri, 5 Nov 2010 23:07:12 GMT+2",
"Content-Length": "0",
"Connection": "close"
},
"body": "",
"strict": true
},
{
"name": "http version 0.9",
"type": "HTTP_RESPONSE",
"raw": "HTTP/0.9 200 OK\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 0,
"http_minor": 9,
"status_code": 200,
"status": "OK",
"num_headers": 0,
"headers": {
},
"body": "",
"strict": true
},
{
"name": "neither content-length nor transfer-encoding response",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello world",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 1,
"headers": {
"Content-Type": "text/plain"
},
"body": "hello world",
"strict": true
},
{
"name": "HTTP/1.0 with keep-alive and EOF-terminated 200 status",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.0 200 OK\r\nConnection: keep-alive\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 0,
"status_code": 200,
"status": "OK",
"num_headers": 1,
"headers": {
"Connection": "keep-alive"
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "HTTP/1.0 with keep-alive and a 204 status",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.0 204 No content\r\nConnection: keep-alive\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 0,
"status_code": 204,
"status": "No content",
"num_headers": 1,
"headers": {
"Connection": "keep-alive"
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "HTTP/1.1 with an EOF-terminated 200 status",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": true,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 0,
"headers": {
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "HTTP/1.1 with a 204 status",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 204 No content\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 204,
"status": "No content",
"num_headers": 0,
"headers": {
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "HTTP/1.1 with a 204 status and keep-alive disabled",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 204 No content\r\nConnection: close\r\n\r\n",
"should_keep_alive": false,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 204,
"status": "No content",
"num_headers": 1,
"headers": {
"Connection": "close"
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "HTTP/1.1 with chunked endocing and a 200 response",
"type": "HTTP_RESPONSE",
"raw": "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 1,
"headers": {
"Transfer-Encoding": "chunked"
},
"body_size": 0,
"body": "",
"strict": true
},
{
"name": "field space",
"type": "HTTP_RESPONSE",
"strict": false,
"raw": "HTTP/1.1 200 OK\r\nServer: Microsoft-IIS/6.0\r\nX-Powered-By: ASP.NET\r\nen-US Content-Type: text/xml\r\nContent-Type: text/xml\r\nContent-Length: 16\r\nDate: Fri, 23 Jul 2010 18:45:38 GMT\r\nConnection: keep-alive\r\n\r\n<xml>hello</xml>",
"should_keep_alive": true,
"message_complete_on_eof": false,
"http_major": 1,
"http_minor": 1,
"status_code": 200,
"status": "OK",
"num_headers": 7,
"headers": {
"Server": "Microsoft-IIS/6.0",
"X-Powered-By": "ASP.NET",
"en-US Content-Type": "text/xml",
"Content-Type": "text/xml",
"Content-Length": "16",
"Date": "Fri, 23 Jul 2010 18:45:38 GMT",
"Connection": "keep-alive"
},
"body": "<xml>hello</xml>"
}
]