Skip to content

Commit 3437318

Browse files
committed
Case insensitive headers
1 parent 74286ea commit 3437318

6 files changed

+53
-31
lines changed

lib/Humming-Bird/Core.rakumod

+20-19
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use Humming-Bird::HTTPServer;
1010

1111
unit module Humming-Bird::Core;
1212

13-
our constant $VERSION = '2.1.7';
13+
our constant $VERSION = '2.2.0';
1414

1515
# Mime type parser from MIME::Types
1616
my constant $mime = MIME::Types.new;
@@ -38,8 +38,8 @@ sub http-method-of-str(Str:D $method --> HTTPMethod:D) {
3838
}
3939

4040
# Converts a string of headers "KEY: VALUE\r\nKEY: VALUE\r\n..." to a map of headers.
41-
sub decode_headers(Str:D $header_block --> Map:D) {
42-
Map.new($header_block.lines.map({ .split(": ", 2, :skip-empty) }).flat);
41+
sub decode-headers(@header_block --> Map:D) {
42+
Map.new(@header_block.map(*.trim.split(': ', 2, :skip-empty).map(*.trim)).map({ [@^a[0].lc, @^a[1]] }).flat);
4343
}
4444

4545
subset SameSite of Str where 'Strict' | 'Lax';
@@ -78,12 +78,13 @@ class HTTPAction {
7878

7979
# Find a header in the action, return (Any) if not found
8080
multi method header(Str:D $name --> Str) {
81-
return Nil without %.headers{$name};
82-
%.headers{$name};
81+
my $lc-name = $name.lc;
82+
return Nil without %.headers{$lc-name};
83+
%.headers{$lc-name};
8384
}
8485

85-
multi method header(Str:D $name, Str:D $value --> HTTPAction:D) {
86-
%.headers{$name} = $value;
86+
multi method header(Str:D $name, Str:D $value) {
87+
%.headers{$name.lc} = $value;
8788
self;
8889
}
8990

@@ -177,24 +178,24 @@ class Request is HTTPAction is export {
177178
my $body = "";
178179

179180
# Lose the request line and parse an assoc list of headers.
180-
my %headers = Map.new(|@split_request[0].split("\r\n", :skip-empty).tail(*-1).map(*.split(':', 2).map(*.trim)).flat);
181+
my %headers = decode-headers(@split_request[0].split("\r\n", :skip-empty).skip(1));
181182

182183
# Body should only exist if either of these headers are present.
183-
with %headers<Content-Length> || %headers<Transfer-Encoding> {
184+
with %headers<content-length> || %headers<transfer-encoding> {
184185
$body = @split_request[1] || $body;
185186
}
186187

187188
# Absolute uris need their path encoded differently.
188-
without %headers<Host> {
189+
without %headers<host> {
189190
my $abs-uri = $path;
190191
$path = $abs-uri.match(/^'http' 's'? '://' <[A..Z a..z \w \. \- \_ 0..9]>+ <('/'.*)>? $/).Str;
191-
%headers<Host> = $abs-uri.match(/^'http''s'?'://'(<-[/]>+)'/'?.* $/)[0].Str;
192+
%headers<host> = $abs-uri.match(/^'http''s'?'://'(<-[/]>+)'/'?.* $/)[0].Str;
192193
}
193194
194195
my %cookies;
195196
# Parse cookies
196-
with %headers<Cookie> {
197-
%cookies := Cookie.decode(%headers<Cookie>);
197+
with %headers<cookie> {
198+
%cookies := Cookie.decode(%headers<cookie>);
198199
}
199200
200201
my $context-id = rand.Str.subst('0.', '').substr: 0, 5;
@@ -237,7 +238,7 @@ class Response is HTTPAction is export {
237238
238239
# Redirect to a given URI, :$permanent allows for a 308 status code vs a 307
239240
method redirect(Str:D $to, :$permanent, :$temporary) {
240-
%.headers<Location> = $to;
241+
self.header('Location', $to);
241242
self.status(303);
242243

243244
self.status(307) if $temporary;
@@ -275,7 +276,7 @@ class Response is HTTPAction is export {
275276
# Write a blob or buffer
276277
method blob(Buf:D $body, Str:D $content-type = 'application/octet-stream', --> Response:D) {
277278
$.body = $body;
278-
%.headers<Content-Type> = $content-type;
279+
self.header('Content-Type', $content-type);
279280
self;
280281
}
281282
# Alias for blob
@@ -285,7 +286,7 @@ class Response is HTTPAction is export {
285286
# Write a string to the body of the response, optionally provide a content type
286287
multi method write(Str:D $body, Str:D $content-type = 'text/plain', --> Response:D) {
287288
$.body = $body;
288-
%.headers<Content-Type> = $content-type;
289+
self.header('Content-Type', $content-type);
289290
self;
290291
}
291292
multi method write(Failure $body, Str:D $content-type = 'text/plain', --> Response:D) {
@@ -296,7 +297,7 @@ class Response is HTTPAction is export {
296297

297298
# Set content type of the response
298299
method content-type(Str:D $type --> Response) {
299-
%.headers<Content-Type> = $type;
300+
self.header('Content-Type', $type);
300301
self;
301302
}
302303

@@ -305,8 +306,8 @@ class Response is HTTPAction is export {
305306
my $out = sprintf("HTTP/1.1 %d $!status\r\n", $!status.code);
306307
my $body-size = $.body ~~ Buf:D ?? $.body.bytes !! $.body.chars;
307308

308-
if $body-size > 0 && %.headers<Content-Type> {
309-
%.headers<Content-Type> ~= '; charset=utf8';
309+
if $body-size > 0 && self.header('Content-Type') && self.header('Content-Type') !~~ /.*'octet-stream'.*/ {
310+
%.headers<content-type> ~= '; charset=utf8';
310311
}
311312

312313
$out ~= sprintf("Content-Length: %d\r\n", $body-size);

t/01-basic.rakutest

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ is routes{'/'}{GET}.path, '/', 'Is route path OK?';
1616
is routes{'/'}{GET}.callback.raku, &cb.raku, 'Is callback OK?';
1717

1818
my $req = Request.new(path => '/', method => GET, version => 'HTTP/1.1');
19-
is routes{'/'}{GET}($req).headers{'Content-Type'}, 'text/html', 'Is response header content type OK?';
19+
is routes{'/'}{GET}($req).header('Content-Type'), 'text/html', 'Is response header content type OK?';
2020
is routes{'/'}{GET}($req).body, 'Hello World', 'Is response body OK?';
2121

2222
post('/', &cb);
2323
is routes{'/'}{POST}.path, '/', 'Is route path OK?';
2424
is routes{'/'}{POST}.callback.raku, &cb.raku, 'Is callback OK?';
2525

2626
$req = Request.new(path => '/', method => POST, version => 'HTTP/1.1');
27-
is routes{'/'}{POST}($req).headers{'Content-Type'}, 'text/html', 'Is response header content type OK?';
27+
is routes{'/'}{POST}($req).header('Content-Type'), 'text/html', 'Is response header content type OK?';
2828
is routes{'/'}{POST}($req).body, 'Hello World', 'Is response body OK?';
2929

3030

t/02-request_encoding.rakutest

+6-6
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ my $simple_header_raw_request = "GET /bob HTTP/1.1\r\nAccepted-Encoding: utf-8\r
1818
my $simple_header_request = Request.decode($simple_header_raw_request);
1919

2020
ok $simple_header_request.method === GET, 'Is method for header request OK?';
21-
is $simple_header_request.headers{'Accepted-Encoding'}, 'utf-8', 'Is header OK?';
21+
is $simple_header_request.header('Accepted-Encoding'), 'utf-8', 'Is header OK?';
2222

2323
my $many_header_raw_request = "GET /bob HTTP/1.1\r\nAccepted-Encoding: utf-8\r\nAccept-Language: en-US\r\nConnection: keep-alive\r\nHost: bob.com\r\n";
2424
my $many_header_request = Request.decode($many_header_raw_request);
2525

26-
is $many_header_request.headers{'Accepted-Encoding'}, 'utf-8', 'Is header 1 OK?';
27-
is $many_header_request.headers{'Accept-Language'}, 'en-US', 'Is header 2 OK?';
28-
is $many_header_request.headers{'Connection'}, 'keep-alive', 'Is header 3 OK?';
26+
is $many_header_request.header('Accepted-Encoding'), 'utf-8', 'Is header 1 OK?';
27+
is $many_header_request.header('Accept-Language'), 'en-US', 'Is header 2 OK?';
28+
is $many_header_request.header('Connection'), 'keep-alive', 'Is header 3 OK?';
2929

3030
my $body = 'aaaaaaaaaa';
3131
my $simple_post_raw_request = "POST / HTTP/1.1\r\nHost: bob.com\r\nContent-Type: application/json\r\nContent-Length: { $body.chars }\r\n\r\n$body";
@@ -41,12 +41,12 @@ is $simple_post_empty_request.body, '', 'Is empty post body OK?';
4141
my $simple-absolute-uri-raw-request = "POST http://localhost/ HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: { $body.chars }\r\n\r\n$body";
4242
my $simple-absolute-uri-request = Request.decode($simple-absolute-uri-raw-request);
4343
is $simple-absolute-uri-request.body, $body, 'Is absolute URI body OK?';
44-
is $simple-absolute-uri-request.headers<Host>, 'localhost', 'Is absolute URI host header OK?';
44+
is $simple-absolute-uri-request.header('Host'), 'localhost', 'Is absolute URI host header OK?';
4545
is $simple-absolute-uri-request.path, '/', 'Is absolute URI path OK?';
4646

4747
my $complex-absolute-uri-raw-request = "POST http://localhost/name/person?bob=123 HTTP/1.1\r\nContent-Type: application/json\r\nContent-Length: { $body.chars }\r\n\r\n$body";
4848
my $complex-absolute-uri-request = Request.decode($complex-absolute-uri-raw-request);
4949
is $complex-absolute-uri-request.body, $body, 'Is absolute URI body OK?';
50-
is $complex-absolute-uri-request.headers<Host>, 'localhost', 'Is absolute URI host header OK?';
50+
is $complex-absolute-uri-request.header('Host'), 'localhost', 'Is absolute URI host header OK?';
5151
is $complex-absolute-uri-request.path, '/name/person', 'Is absolute URI path OK?';
5252
is $complex-absolute-uri-request.query('bob'), '123', 'Is query param OK?';

t/03-response_decoding.rakutest

+7-3
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ use Test;
66
use Humming-Bird::Core;
77
use HTTP::Status;
88

9-
plan 3;
9+
plan 5;
1010

1111
my $initiator = Request.new(path => '/', method => GET, version => 'HTTP/1.1');
1212
my $simple_response = Response.new(:$initiator, status => HTTP::Status(200));
1313

1414
ok $simple_response.encode, 'Does decode not die?';
1515

16-
my %headers = 'Content-Length', 10, 'Encoding', 'utf-8';
17-
my $simple_response_headers = Response.new(:$initiator, status => HTTP::Status(200), :%headers);
16+
my $simple_response_headers = Response.new(:$initiator, status => HTTP::Status(200));
17+
18+
$simple_response_headers.header('Content-Length', 10.Str).header('Encoding', 'utf-8');
19+
20+
ok $simple_response_headers.header('encoding');
21+
ok $simple_response_headers.header('content-LENGTH');
1822

1923
ok $simple_response_headers.encode, 'Does encode with headers not die?';
2024
$simple_response_headers.write('abc');

t/08-static.rakutest

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ ok routes{'/'}{'static'}, 'Is route OK?';
1313
ok routes{'/'}{'static'}{GET}, 'Is route method OK?';
1414
is routes{'/'}{'static'}{GET}(Request.new(path => 't/static/test.css', method => GET, version => 'HTTP/1.1')).status, HTTP::Status(200), 'Is response status OK?';
1515
is routes{'/'}{'static'}{GET}(Request.new(path => 't/static/test.css', method => GET, version => 'HTTP/1.1')).body.chomp, q<img { color: 'blue'; }>, 'Is response body OK?';
16-
is routes{'/'}{'static'}{GET}(Request.new(path => 't/static/test.css', method => GET, version => 'HTTP/1.1')).headers<Content-Type>, 'text/css', 'Is content-type OK?';
16+
is routes{'/'}{'static'}{GET}(Request.new(path => 't/static/test.css', method => GET, version => 'HTTP/1.1')).header('Content-Type'), 'text/css', 'Is content-type OK?';
1717
is routes{'/'}{'static'}{GET}(Request.new(path => 't/static/test.css.bob', method => GET, version => 'HTTP/1.1')).status, HTTP::Status(404), 'Is missing response status OK?';
1818

1919
done-testing;

t/12-headers.rakutest

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use v6;
2+
use lib 'lib';
3+
use strict;
4+
use Test;
5+
use Humming-Bird::Core;
6+
7+
plan 4;
8+
9+
my $req = Request.new(path => '/', method => GET, version => 'HTTP/1.1');
10+
11+
ok $req.header('Foo', 'bar'), 'Does add ok?';
12+
ok $req.header('Bar', 'foo'), 'Does add ok?';
13+
14+
is $req.header('foo'), 'bar', 'Does get case insensitive?';
15+
is $req.header('BaR'), 'foo', 'Does get case insensitive?';
16+
17+
done-testing;

0 commit comments

Comments
 (0)