Description
When receiving a response with a header that is folded over multiple lines, the following error is returned:
Protocol::HTTP1::BadHeader: Could not parse header
I first noticed this issue when trying to migrate to using the async-http-faraday
adapter - some requests to external APIs started returning these responses. I was able to reproduce this behaviour by adapting the example from the readme with a simple localhost NGINX server that returns a multi-line header:
repro.rb
require 'async'
require 'io/stream'
require 'async/http/endpoint'
require 'protocol/http1/connection'
Async do
endpoint = Async::HTTP::Endpoint.parse("http://localhost:8042", alpn_protocols: ["http/1.1"])
peer = endpoint.connect
puts "Connected to #{peer} #{peer.remote_address.inspect}"
# IO Buffering...
stream = IO::Stream::Buffered.new(peer)
client = Protocol::HTTP1::Connection.new(stream)
def client.read_line
@stream.read_until(Protocol::HTTP1::Connection::CRLF) or raise EOFError
end
puts "Writing request..."
client.write_request("localhost:8042", "GET", "/", "HTTP/1.1", [["Accept", "*/*"]])
client.write_body("HTTP/1.1", nil)
puts "Reading response..."
response = client.read_response("GET")
puts "Got response: #{response.inspect}"
puts "Closing client..."
client.close
end
nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
add_header "Test-Multiline-Header" "foo;
bar";
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
Start an NGINX container with this configuration and run the repro script to observe the error:
$ docker run --name test-nginx -v ./nginx.conf:/etc/nginx/nginx.conf -d -p 8042:80 nginx
66c3263f56467000d11fec50ffe76e3b29d3f3cc2e1396a559c801ea80313be6
$ ruby repro.rb
Connected to #<Socket:0x00000001206bbab8> #<Addrinfo: [::1]:8042 TCP>
Writing request...
Reading response...
0.0s warn: Async::Task [oid=0x230] [ec=0x244] [pid=24080] [2024-09-12 16:40:27 +0300]
| Task may have ended with unhandled exception.
| Protocol::HTTP1::BadHeader: Could not parse header: "Test-Multiline-Header: foo;\n\t\tbar"
| → /Users/vytautaskubilius/.rbenv/versions/3.3.3/lib/ruby/gems/3.3.0/gems/protocol-http1-0.22.0/lib/protocol/http1/connection.rb:243 in `read_headers'
| /Users/vytautaskubilius/.rbenv/versions/3.3.3/lib/ruby/gems/3.3.0/gems/protocol-http1-0.22.0/lib/protocol/http1/connection.rb:222 in `read_response'
| repro.rb:26 in `block in <main>'
| /Users/vytautaskubilius/.rbenv/versions/3.3.3/lib/ruby/gems/3.3.0/gems/async-2.16.1/lib/async/task.rb:197 in `block in run'
| /Users/vytautaskubilius/.rbenv/versions/3.3.3/lib/ruby/gems/3.3.0/gems/async-2.16.1/lib/async/task.rb:422 in `block in schedule'
The HTTP/1.1 specs indicate that multiline header values are acceptable as long at they start with horizontal tabs or spaces:
HTTP/1.1 header field values can be folded onto multiple lines if the
continuation line begins with a space or horizontal tab. All linear
white space, including folding, has the same semantics as SP. A
recipient MAY replace any linear white space with a single SP before
interpreting the field value or forwarding the message downstream.