summaryrefslogtreecommitdiffstats
path: root/llhttp/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:56:19 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:56:19 +0000
commit0b6210cd37b68b94252cb798598b12974a20e1c1 (patch)
treee371686554a877842d95aa94f100bee552ff2a8e /llhttp/test
parentInitial commit. (diff)
downloadnode-undici-0b6210cd37b68b94252cb798598b12974a20e1c1.tar.xz
node-undici-0b6210cd37b68b94252cb798598b12974a20e1c1.zip
Adding upstream version 5.28.2+dfsg1+~cs23.11.12.3.upstream/5.28.2+dfsg1+_cs23.11.12.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'llhttp/test')
-rw-r--r--llhttp/test/fixtures/extra.c457
-rw-r--r--llhttp/test/fixtures/index.ts116
-rw-r--r--llhttp/test/fuzzers/fuzz_parser.c45
-rw-r--r--llhttp/test/md-test.ts269
-rw-r--r--llhttp/test/request/connection.md732
-rw-r--r--llhttp/test/request/content-length.md482
-rw-r--r--llhttp/test/request/finish.md69
-rw-r--r--llhttp/test/request/invalid.md607
-rw-r--r--llhttp/test/request/lenient-headers.md145
-rw-r--r--llhttp/test/request/lenient-version.md23
-rw-r--r--llhttp/test/request/method.md450
-rw-r--r--llhttp/test/request/pausing.md381
-rw-r--r--llhttp/test/request/pipelining.md66
-rw-r--r--llhttp/test/request/sample.md629
-rw-r--r--llhttp/test/request/transfer-encoding.md1187
-rw-r--r--llhttp/test/request/uri.md243
-rw-r--r--llhttp/test/response/connection.md647
-rw-r--r--llhttp/test/response/content-length.md158
-rw-r--r--llhttp/test/response/finish.md23
-rw-r--r--llhttp/test/response/invalid.md285
-rw-r--r--llhttp/test/response/lenient-version.md20
-rw-r--r--llhttp/test/response/pausing.md330
-rw-r--r--llhttp/test/response/pipelining.md60
-rw-r--r--llhttp/test/response/sample.md653
-rw-r--r--llhttp/test/response/transfer-encoding.md410
-rw-r--r--llhttp/test/url.md261
26 files changed, 8748 insertions, 0 deletions
diff --git a/llhttp/test/fixtures/extra.c b/llhttp/test/fixtures/extra.c
new file mode 100644
index 0000000..dadf8dc
--- /dev/null
+++ b/llhttp/test/fixtures/extra.c
@@ -0,0 +1,457 @@
+#include <stdlib.h>
+
+#include "fixture.h"
+
+int llhttp__on_url(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("url", p, endp);
+}
+
+
+int llhttp__on_url_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "url complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_URL_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_url_schema(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("url.schema", p, endp);
+}
+
+
+int llhttp__on_url_host(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("url.host", p, endp);
+}
+
+
+int llhttp__on_url_path(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("url.path", p, endp);
+}
+
+
+int llhttp__on_url_query(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("url.query", p, endp);
+}
+
+
+int llhttp__on_url_fragment(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("url.fragment", p, endp);
+}
+
+
+#ifdef LLHTTP__TEST_HTTP
+
+void llhttp__test_init_request(llparse_t* s) {
+ s->type = HTTP_REQUEST;
+}
+
+
+void llhttp__test_init_response(llparse_t* s) {
+ s->type = HTTP_RESPONSE;
+}
+
+
+void llhttp__test_init_request_lenient_all(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |=
+ LENIENT_HEADERS | LENIENT_CHUNKED_LENGTH | LENIENT_KEEP_ALIVE |
+ LENIENT_TRANSFER_ENCODING | LENIENT_VERSION | LENIENT_DATA_AFTER_CLOSE |
+ LENIENT_OPTIONAL_LF_AFTER_CR | LENIENT_OPTIONAL_CR_BEFORE_LF |
+ LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
+}
+
+
+void llhttp__test_init_response_lenient_all(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |=
+ LENIENT_HEADERS | LENIENT_CHUNKED_LENGTH | LENIENT_KEEP_ALIVE |
+ LENIENT_TRANSFER_ENCODING | LENIENT_VERSION | LENIENT_DATA_AFTER_CLOSE |
+ LENIENT_OPTIONAL_LF_AFTER_CR | LENIENT_OPTIONAL_CR_BEFORE_LF |
+ LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
+}
+
+
+void llhttp__test_init_request_lenient_headers(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_HEADERS;
+}
+
+
+void llhttp__test_init_request_lenient_chunked_length(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_CHUNKED_LENGTH;
+}
+
+
+void llhttp__test_init_request_lenient_keep_alive(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_KEEP_ALIVE;
+}
+
+void llhttp__test_init_request_lenient_transfer_encoding(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_TRANSFER_ENCODING;
+}
+
+
+void llhttp__test_init_request_lenient_version(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_VERSION;
+}
+
+
+void llhttp__test_init_response_lenient_keep_alive(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_KEEP_ALIVE;
+}
+
+void llhttp__test_init_response_lenient_version(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_VERSION;
+}
+
+
+void llhttp__test_init_response_lenient_headers(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_HEADERS;
+}
+
+void llhttp__test_init_request_lenient_data_after_close(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_DATA_AFTER_CLOSE;
+}
+
+void llhttp__test_init_response_lenient_data_after_close(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_DATA_AFTER_CLOSE;
+}
+
+void llhttp__test_init_request_lenient_optional_lf_after_cr(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_OPTIONAL_LF_AFTER_CR;
+}
+
+void llhttp__test_init_response_lenient_optional_lf_after_cr(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_OPTIONAL_LF_AFTER_CR;
+}
+
+void llhttp__test_init_request_lenient_optional_cr_before_lf(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_OPTIONAL_CR_BEFORE_LF;
+}
+
+void llhttp__test_init_response_lenient_optional_cr_before_lf(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_OPTIONAL_CR_BEFORE_LF;
+}
+
+void llhttp__test_init_request_lenient_optional_crlf_after_chunk(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
+}
+
+void llhttp__test_init_response_lenient_optional_crlf_after_chunk(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_OPTIONAL_CRLF_AFTER_CHUNK;
+}
+
+void llhttp__test_init_request_lenient_spaces_after_chunk_size(llparse_t* s) {
+ llhttp__test_init_request(s);
+ s->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
+}
+
+void llhttp__test_init_response_lenient_spaces_after_chunk_size(llparse_t* s) {
+ llhttp__test_init_response(s);
+ s->lenient_flags |= LENIENT_SPACES_AFTER_CHUNK_SIZE;
+}
+
+
+void llhttp__test_finish(llparse_t* s) {
+ llparse__print(NULL, NULL, "finish=%d", s->finish);
+}
+
+
+int llhttp__on_message_begin(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "message begin");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_MESSAGE_BEGIN
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_message_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "message complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_MESSAGE_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_status(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("status", p, endp);
+}
+
+
+int llhttp__on_status_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "status complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_STATUS_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_method(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench || s->type != HTTP_REQUEST)
+ return 0;
+
+ return llparse__print_span("method", p, endp);
+}
+
+
+int llhttp__on_method_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "method complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_METHOD_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_version(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("version", p, endp);
+}
+
+
+int llhttp__on_version_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "version complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_VERSION_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+int llhttp__on_header_field(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("header_field", p, endp);
+}
+
+
+int llhttp__on_header_field_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "header_field complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_HEADER_FIELD_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_header_value(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("header_value", p, endp);
+}
+
+
+int llhttp__on_header_value_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "header_value complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_HEADER_VALUE_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_headers_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ if (s->type == HTTP_REQUEST) {
+ llparse__print(p, endp,
+ "headers complete method=%d v=%d/%d flags=%x content_length=%llu",
+ s->method, s->http_major, s->http_minor, s->flags, s->content_length);
+ } else if (s->type == HTTP_RESPONSE) {
+ llparse__print(p, endp,
+ "headers complete status=%d v=%d/%d flags=%x content_length=%llu",
+ s->status_code, s->http_major, s->http_minor, s->flags,
+ s->content_length);
+ } else {
+ llparse__print(p, endp, "invalid headers complete");
+ }
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_HEADERS_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #elif defined(LLHTTP__TEST_SKIP_BODY)
+ llparse__print(p, endp, "skip body");
+ return 1;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_body(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("body", p, endp);
+}
+
+
+int llhttp__on_chunk_header(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "chunk header len=%d", (int) s->content_length);
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_CHUNK_HEADER
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_chunk_extension_name(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("chunk_extension_name", p, endp);
+}
+
+
+int llhttp__on_chunk_extension_name_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "chunk_extension_name complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_CHUNK_EXTENSION_NAME
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_chunk_extension_value(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ return llparse__print_span("chunk_extension_value", p, endp);
+}
+
+
+int llhttp__on_chunk_extension_value_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "chunk_extension_value complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_CHUNK_EXTENSION_VALUE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+
+int llhttp__on_chunk_complete(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "chunk complete");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_CHUNK_COMPLETE
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+int llhttp__on_reset(llparse_t* s, const char* p, const char* endp) {
+ if (llparse__in_bench)
+ return 0;
+
+ llparse__print(p, endp, "reset");
+
+ #ifdef LLHTTP__TEST_PAUSE_ON_RESET
+ return LLPARSE__ERROR_PAUSE;
+ #else
+ return 0;
+ #endif
+}
+
+#endif /* LLHTTP__TEST_HTTP */
diff --git a/llhttp/test/fixtures/index.ts b/llhttp/test/fixtures/index.ts
new file mode 100644
index 0000000..1571f9d
--- /dev/null
+++ b/llhttp/test/fixtures/index.ts
@@ -0,0 +1,116 @@
+import * as fs from 'fs';
+import { ICompilerResult, LLParse } from 'llparse';
+import { Dot } from 'llparse-dot';
+import {
+ Fixture, FixtureResult, IFixtureBuildOptions,
+} from 'llparse-test-fixture';
+import * as path from 'path';
+
+import * as llhttp from '../../src/llhttp';
+
+export { FixtureResult };
+
+export type TestType = 'request' | 'response' | 'request-finish' | 'response-finish' |
+ 'request-lenient-all' | 'response-lenient-all' |
+ 'request-lenient-headers' | 'response-lenient-headers' |
+ 'request-lenient-chunked-length' | 'request-lenient-transfer-encoding' |
+ 'request-lenient-keep-alive' | 'response-lenient-keep-alive' |
+ 'request-lenient-version' | 'response-lenient-version' |
+ 'request-lenient-data-after-close' | 'response-lenient-data-after-close' |
+ 'request-lenient-optional-lf-after-cr' | 'response-lenient-optional-lf-after-cr' |
+ 'request-lenient-optional-cr-before-lf' | 'response-lenient-optional-cr-before-lf' |
+ 'request-lenient-optional-crlf-after-chunk' | 'response-lenient-optional-crlf-after-chunk' |
+ 'request-lenient-spaces-after-chunk-size' | 'response-lenient-spaces-after-chunk-size' |
+ 'none' | 'url';
+
+export const allowedTypes: TestType[] = [
+ 'request',
+ 'response',
+ 'request-finish',
+ 'response-finish',
+ 'request-lenient-all',
+ 'response-lenient-all',
+ 'request-lenient-headers',
+ 'response-lenient-headers',
+ 'request-lenient-keep-alive',
+ 'response-lenient-keep-alive',
+ 'request-lenient-chunked-length',
+ 'request-lenient-transfer-encoding',
+ 'request-lenient-version',
+ 'response-lenient-version',
+ 'request-lenient-data-after-close',
+ 'response-lenient-data-after-close',
+ 'request-lenient-optional-lf-after-cr',
+ 'response-lenient-optional-lf-after-cr',
+ 'request-lenient-optional-cr-before-lf',
+ 'response-lenient-optional-cr-before-lf',
+ 'request-lenient-optional-crlf-after-chunk',
+ 'response-lenient-optional-crlf-after-chunk',
+ 'request-lenient-spaces-after-chunk-size',
+ 'response-lenient-spaces-after-chunk-size',
+];
+
+const BUILD_DIR = path.join(__dirname, '..', 'tmp');
+const CHEADERS_FILE = path.join(BUILD_DIR, 'cheaders.h');
+
+const cheaders = new llhttp.CHeaders().build();
+try {
+ fs.mkdirSync(BUILD_DIR);
+} catch (e) {
+ // no-op
+}
+fs.writeFileSync(CHEADERS_FILE, cheaders);
+
+const fixtures = new Fixture({
+ buildDir: path.join(__dirname, '..', 'tmp'),
+ extra: [
+ '-msse4.2',
+ '-DLLHTTP__TEST',
+ '-DLLPARSE__ERROR_PAUSE=' + llhttp.constants.ERROR.PAUSED,
+ '-include', CHEADERS_FILE,
+ path.join(__dirname, 'extra.c'),
+ ],
+ maxParallel: process.env.LLPARSE_DEBUG ? 1 : undefined,
+});
+
+const cache: Map<any, ICompilerResult> = new Map();
+
+export async function build(
+ llparse: LLParse, node: any, outFile: string,
+ options: IFixtureBuildOptions = {},
+ ty: TestType = 'none'): Promise<FixtureResult> {
+ const dot = new Dot();
+ fs.writeFileSync(path.join(BUILD_DIR, outFile + '.dot'),
+ dot.build(node));
+
+ let artifacts: ICompilerResult;
+ if (cache.has(node)) {
+ artifacts = cache.get(node)!;
+ } else {
+ artifacts = llparse.build(node, {
+ c: { header: outFile },
+ debug: process.env.LLPARSE_DEBUG ? 'llparse__debug' : undefined,
+ });
+ cache.set(node, artifacts);
+ }
+
+ const extra = options.extra === undefined ? [] : options.extra.slice();
+
+ if (allowedTypes.includes(ty)) {
+ extra.push(
+ `-DLLPARSE__TEST_INIT=llhttp__test_init_${ty.replace(/-/g, '_')}`);
+ }
+
+ if (ty === 'request-finish' || ty === 'response-finish') {
+ if (ty === 'request-finish') {
+ extra.push('-DLLPARSE__TEST_INIT=llhttp__test_init_request');
+ } else {
+ extra.push('-DLLPARSE__TEST_INIT=llhttp__test_init_response');
+ }
+ extra.push('-DLLPARSE__TEST_FINISH=llhttp__test_finish');
+ }
+
+ return await fixtures.build(artifacts, outFile, Object.assign(options, {
+ extra,
+ }));
+}
diff --git a/llhttp/test/fuzzers/fuzz_parser.c b/llhttp/test/fuzzers/fuzz_parser.c
new file mode 100644
index 0000000..60d00ae
--- /dev/null
+++ b/llhttp/test/fuzzers/fuzz_parser.c
@@ -0,0 +1,45 @@
+#include "llhttp.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+int handle_on_message_complete(llhttp_t *arg) { return 0; }
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ llhttp_t parser;
+ llhttp_settings_t settings;
+ llhttp_type_t http_type;
+
+ /* We need four bytes to determine variable parameters */
+ if (size < 4) {
+ return 0;
+ }
+
+ int headers = (data[0] & 0x01) == 1;
+ int chunked_length = (data[1] & 0x01) == 1;
+ int keep_alive = (data[2] & 0x01) == 1;
+ if (data[0] % 3 == 0) {
+ http_type = HTTP_BOTH;
+ } else if (data[0] % 3 == 1) {
+ http_type = HTTP_REQUEST;
+ } else {
+ http_type = HTTP_RESPONSE;
+ }
+ data += 4;
+ size -= 4;
+
+ /* Initialize user callbacks and settings */
+ llhttp_settings_init(&settings);
+
+ /* Set user callback */
+ settings.on_message_complete = handle_on_message_complete;
+
+ llhttp_init(&parser, http_type, &settings);
+ llhttp_set_lenient_headers(&parser, headers);
+ llhttp_set_lenient_chunked_length(&parser, chunked_length);
+ llhttp_set_lenient_keep_alive(&parser, keep_alive);
+
+ llhttp_execute(&parser, data, size);
+
+ return 0;
+}
diff --git a/llhttp/test/md-test.ts b/llhttp/test/md-test.ts
new file mode 100644
index 0000000..0c24e18
--- /dev/null
+++ b/llhttp/test/md-test.ts
@@ -0,0 +1,269 @@
+import * as assert from 'assert';
+import * as fs from 'fs';
+import { LLParse } from 'llparse';
+import { Group, MDGator, Metadata, Test } from 'mdgator';
+import * as path from 'path';
+import * as vm from 'vm';
+
+import * as llhttp from '../src/llhttp';
+import {IHTTPResult} from '../src/llhttp/http';
+import {IURLResult} from '../src/llhttp/url';
+import { allowedTypes, build, FixtureResult, TestType } from './fixtures';
+
+//
+// Cache nodes/llparse instances ahead of time
+// (different types of tests will re-use them)
+//
+
+interface INodeCacheEntry {
+ llparse: LLParse;
+ entry: IHTTPResult['entry'];
+}
+
+interface IUrlCacheEntry {
+ llparse: LLParse;
+ entry: IURLResult['entry']['normal'];
+}
+
+const modeCache = new Map<string, FixtureResult>();
+
+function buildNode() {
+ const p = new LLParse();
+ const instance = new llhttp.HTTP(p);
+
+ return { llparse: p, entry: instance.build().entry };
+}
+
+function buildURL() {
+ const p = new LLParse();
+ const instance = new llhttp.URL(p, true);
+
+ const node = instance.build();
+
+ // Loop
+ node.exit.toHTTP.otherwise(node.entry.normal);
+ node.exit.toHTTP09.otherwise(node.entry.normal);
+
+ return { llparse: p, entry: node.entry.normal };
+}
+
+//
+// Build binaries using cached nodes/llparse
+//
+
+async function buildMode(ty: TestType, meta: any)
+ : Promise<FixtureResult> {
+
+ const cacheKey = `${ty}:${JSON.stringify(meta || {})}`;
+ let entry = modeCache.get(cacheKey);
+
+ if (entry) {
+ return entry;
+ }
+
+ let node;
+ let prefix: string;
+ let extra: string[];
+ if (ty === 'url') {
+ node = buildURL();
+ prefix = 'url';
+ extra = [];
+ } else {
+ node = buildNode();
+ prefix = 'http';
+ extra = [
+ '-DLLHTTP__TEST_HTTP',
+ path.join(__dirname, '..', 'src', 'native', 'http.c'),
+ ];
+ }
+
+ if (meta.pause) {
+ extra.push(`-DLLHTTP__TEST_PAUSE_${meta.pause.toUpperCase()}=1`);
+ }
+
+ if (meta.skipBody) {
+ extra.push('-DLLHTTP__TEST_SKIP_BODY=1');
+ }
+
+ entry = await build(node.llparse, node.entry, `${prefix}-${ty}`, {
+ extra,
+ }, ty);
+
+ modeCache.set(cacheKey, entry);
+ return entry;
+}
+
+interface IFixtureMap {
+ [key: string]: { [key: string]: Promise<FixtureResult> };
+}
+
+//
+// Run test suite
+//
+
+function run(name: string): void {
+ const md = new MDGator();
+
+ const raw = fs.readFileSync(path.join(__dirname, name + '.md')).toString();
+ const groups = md.parse(raw);
+
+ function runSingleTest(ty: TestType, meta: any,
+ input: string,
+ expected: ReadonlyArray<string | RegExp>): void {
+ it(`should pass for type="${ty}"`, async () => {
+ const binary = await buildMode(ty, meta);
+ await binary.check(input, expected, {
+ noScan: meta.noScan === true,
+ });
+ });
+ }
+
+ function runTest(test: Test) {
+ describe(test.name + ` at ${name}.md:${test.line + 1}`, () => {
+ let types: TestType[] = [];
+
+ const isURL = test.values.has('url');
+ const inputKey = isURL ? 'url' : 'http';
+
+ assert(test.values.has(inputKey),
+ `Missing "${inputKey}" code in md file`);
+ assert.strictEqual(test.values.get(inputKey)!.length, 1,
+ `Expected just one "${inputKey}" input`);
+
+ let meta: Metadata;
+ if (test.meta.has(inputKey)) {
+ meta = test.meta.get(inputKey)![0]!;
+ } else {
+ assert(isURL, 'Missing required http metadata');
+ meta = {};
+ }
+
+ if (isURL) {
+ types = [ 'url' ];
+ } else {
+ assert(meta.hasOwnProperty('type'), 'Missing required `type` metadata');
+
+ if (meta.type) {
+ if (!allowedTypes.includes(meta.type)) {
+ throw new Error(`Invalid value of \`type\` metadata: "${meta.type}"`);
+ }
+
+ types.push(meta.type);
+ }
+ }
+
+ assert(test.values.has('log'), 'Missing `log` code in md file');
+
+ assert.strictEqual(test.values.get('log')!.length, 1,
+ 'Expected just one output');
+
+ let input: string = test.values.get(inputKey)![0];
+ let expected: string = test.values.get('log')![0];
+
+ // Remove trailing newline
+ input = input.replace(/\n$/, '');
+
+ // Remove escaped newlines
+ input = input.replace(/\\(\r\n|\r|\n)/g, '');
+
+ // Normalize all newlines
+ input = input.replace(/\r\n|\r|\n/g, '\r\n');
+
+ // Replace escaped CRLF, tabs, form-feed
+ input = input.replace(/\\r/g, '\r');
+ input = input.replace(/\\n/g, '\n');
+ input = input.replace(/\\t/g, '\t');
+ input = input.replace(/\\f/g, '\f');
+ input = input.replace(/\\x([0-9a-fA-F]+)/g, (all, hex) => {
+ return String.fromCharCode(parseInt(hex, 16));
+ });
+
+ // Useful in token tests
+ input = input.replace(/\\([0-7]{1,3})/g, (_, digits) => {
+ return String.fromCharCode(parseInt(digits, 8));
+ });
+
+ // Evaluate inline JavaScript
+ input = input.replace(/\$\{(.+?)\}/g, (_, code) => {
+ return vm.runInNewContext(code) + '';
+ });
+
+ // Escape first symbol `\r` or `\n`, `|`, `&` for Windows
+ if (process.platform === 'win32') {
+ const firstByte = Buffer.from(input)[0];
+ if (firstByte === 0x0a || firstByte === 0x0d) {
+ input = '\\' + input;
+ }
+
+ input = input.replace(/\|/g, '^|');
+ input = input.replace(/&/g, '^&');
+ }
+
+ // Replace escaped tabs/form-feed in expected too
+ expected = expected.replace(/\\t/g, '\t');
+ expected = expected.replace(/\\f/g, '\f');
+
+ // Split
+ const expectedLines = expected.split(/\n/g).slice(0, -1);
+
+ const fullExpected = expectedLines.map((line) => {
+ if (line.startsWith('/')) {
+ return new RegExp(line.trim().slice(1, -1));
+ } else {
+ return line;
+ }
+ });
+
+ for (const ty of types) {
+ if (meta.skip === true || (process.env.ONLY === 'true' && !meta.only)) {
+ continue;
+ }
+
+ runSingleTest(ty, meta, input, fullExpected);
+ }
+ });
+ }
+
+ function runGroup(group: Group) {
+ describe(group.name + ` at ${name}.md:${group.line + 1}`, function() {
+ this.timeout(60000);
+
+ for (const child of group.children) {
+ runGroup(child);
+ }
+
+ for (const test of group.tests) {
+ runTest(test);
+ }
+ });
+ }
+
+ for (const group of groups) {
+ runGroup(group);
+ }
+}
+
+run('request/sample');
+run('request/lenient-headers');
+run('request/lenient-version');
+run('request/method');
+run('request/uri');
+run('request/connection');
+run('request/content-length');
+run('request/transfer-encoding');
+run('request/invalid');
+run('request/finish');
+run('request/pausing');
+run('request/pipelining');
+
+run('response/sample');
+run('response/connection');
+run('response/content-length');
+run('response/transfer-encoding');
+run('response/invalid');
+run('response/finish');
+run('response/lenient-version');
+run('response/pausing');
+run('response/pipelining');
+
+run('url');
diff --git a/llhttp/test/request/connection.md b/llhttp/test/request/connection.md
new file mode 100644
index 0000000..a03242e
--- /dev/null
+++ b/llhttp/test/request/connection.md
@@ -0,0 +1,732 @@
+Connection header
+=================
+
+## `keep-alive`
+
+### Setting flag
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: keep-alive
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=10 span[header_value]="keep-alive"
+off=43 header_value complete
+off=45 headers complete method=4 v=1/1 flags=1 content_length=0
+off=45 message complete
+```
+
+### Restarting when keep-alive is explicitly
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: keep-alive
+
+PUT /url HTTP/1.1
+Connection: keep-alive
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=10 span[header_value]="keep-alive"
+off=43 header_value complete
+off=45 headers complete method=4 v=1/1 flags=1 content_length=0
+off=45 message complete
+off=45 reset
+off=45 message begin
+off=45 len=3 span[method]="PUT"
+off=48 method complete
+off=49 len=4 span[url]="/url"
+off=54 url complete
+off=59 len=3 span[version]="1.1"
+off=62 version complete
+off=64 len=10 span[header_field]="Connection"
+off=75 header_field complete
+off=76 len=10 span[header_value]="keep-alive"
+off=88 header_value complete
+off=90 headers complete method=4 v=1/1 flags=1 content_length=0
+off=90 message complete
+```
+
+### No restart when keep-alive is off (1.0)
+
+<!-- meta={"type": "request" } -->
+```http
+PUT /url HTTP/1.0
+
+PUT /url HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.0"
+off=17 version complete
+off=21 headers complete method=4 v=1/0 flags=0 content_length=0
+off=21 message complete
+off=22 error code=5 reason="Data after `Connection: close`"
+```
+
+### Resetting flags when keep-alive is off (1.0, lenient)
+
+Even though we allow restarts in loose mode, the flags should be still set to
+`0` upon restart.
+
+<!-- meta={"type": "request-lenient-keep-alive"} -->
+```http
+PUT /url HTTP/1.0
+Content-Length: 0
+
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.0"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=1 span[header_value]="0"
+off=38 header_value complete
+off=40 headers complete method=4 v=1/0 flags=20 content_length=0
+off=40 message complete
+off=40 reset
+off=40 message begin
+off=40 len=3 span[method]="PUT"
+off=43 method complete
+off=44 len=4 span[url]="/url"
+off=49 url complete
+off=54 len=3 span[version]="1.1"
+off=57 version complete
+off=59 len=17 span[header_field]="Transfer-Encoding"
+off=77 header_field complete
+off=78 len=7 span[header_value]="chunked"
+off=87 header_value complete
+off=89 headers complete method=4 v=1/1 flags=208 content_length=0
+```
+
+### CRLF between requests, implicit `keep-alive`
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Host: www.example.com
+Content-Type: application/x-www-form-urlencoded
+Content-Length: 4
+
+q=42
+
+GET / HTTP/1.1
+```
+_Note the trailing CRLF above_
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=15 span[header_value]="www.example.com"
+off=40 header_value complete
+off=40 len=12 span[header_field]="Content-Type"
+off=53 header_field complete
+off=54 len=33 span[header_value]="application/x-www-form-urlencoded"
+off=89 header_value complete
+off=89 len=14 span[header_field]="Content-Length"
+off=104 header_field complete
+off=105 len=1 span[header_value]="4"
+off=108 header_value complete
+off=110 headers complete method=3 v=1/1 flags=20 content_length=4
+off=110 len=4 span[body]="q=42"
+off=114 message complete
+off=118 reset
+off=118 message begin
+off=118 len=3 span[method]="GET"
+off=121 method complete
+off=122 len=1 span[url]="/"
+off=124 url complete
+off=129 len=3 span[version]="1.1"
+off=132 version complete
+```
+
+### Not treating `\r` as `-`
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: keep\ralive
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=4 span[header_value]="keep"
+off=36 error code=3 reason="Missing expected LF after header value"
+```
+
+## `close`
+
+### Setting flag on `close`
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: close
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=5 span[header_value]="close"
+off=38 header_value complete
+off=40 headers complete method=4 v=1/1 flags=2 content_length=0
+off=40 message complete
+```
+
+### CRLF between requests, explicit `close`
+
+`close` means closed connection
+
+<!-- meta={"type": "request" } -->
+```http
+POST / HTTP/1.1
+Host: www.example.com
+Content-Type: application/x-www-form-urlencoded
+Content-Length: 4
+Connection: close
+
+q=42
+
+GET / HTTP/1.1
+```
+_Note the trailing CRLF above_
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=15 span[header_value]="www.example.com"
+off=40 header_value complete
+off=40 len=12 span[header_field]="Content-Type"
+off=53 header_field complete
+off=54 len=33 span[header_value]="application/x-www-form-urlencoded"
+off=89 header_value complete
+off=89 len=14 span[header_field]="Content-Length"
+off=104 header_field complete
+off=105 len=1 span[header_value]="4"
+off=108 header_value complete
+off=108 len=10 span[header_field]="Connection"
+off=119 header_field complete
+off=120 len=5 span[header_value]="close"
+off=127 header_value complete
+off=129 headers complete method=3 v=1/1 flags=22 content_length=4
+off=129 len=4 span[body]="q=42"
+off=133 message complete
+off=138 error code=5 reason="Data after `Connection: close`"
+```
+
+### CRLF between requests, explicit `close` (lenient)
+
+Loose mode is more lenient, and allows further requests.
+
+<!-- meta={"type": "request-lenient-keep-alive"} -->
+```http
+POST / HTTP/1.1
+Host: www.example.com
+Content-Type: application/x-www-form-urlencoded
+Content-Length: 4
+Connection: close
+
+q=42
+
+GET / HTTP/1.1
+```
+_Note the trailing CRLF above_
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=15 span[header_value]="www.example.com"
+off=40 header_value complete
+off=40 len=12 span[header_field]="Content-Type"
+off=53 header_field complete
+off=54 len=33 span[header_value]="application/x-www-form-urlencoded"
+off=89 header_value complete
+off=89 len=14 span[header_field]="Content-Length"
+off=104 header_field complete
+off=105 len=1 span[header_value]="4"
+off=108 header_value complete
+off=108 len=10 span[header_field]="Connection"
+off=119 header_field complete
+off=120 len=5 span[header_value]="close"
+off=127 header_value complete
+off=129 headers complete method=3 v=1/1 flags=22 content_length=4
+off=129 len=4 span[body]="q=42"
+off=133 message complete
+off=137 reset
+off=137 message begin
+off=137 len=3 span[method]="GET"
+off=140 method complete
+off=141 len=1 span[url]="/"
+off=143 url complete
+off=148 len=3 span[version]="1.1"
+off=151 version complete
+```
+
+## Parsing multiple tokens
+
+### Sample
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: close, token, upgrade, token, keep-alive
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=40 span[header_value]="close, token, upgrade, token, keep-alive"
+off=73 header_value complete
+off=75 headers complete method=4 v=1/1 flags=7 content_length=0
+off=75 message complete
+```
+
+### Multiple tokens with folding
+
+<!-- meta={"type": "request"} -->
+```http
+GET /demo HTTP/1.1
+Host: example.com
+Connection: Something,
+ Upgrade, ,Keep-Alive
+Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
+Sec-WebSocket-Protocol: sample
+Upgrade: WebSocket
+Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
+Origin: http://example.com
+
+Hot diggity dogg
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=5 span[url]="/demo"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=20 len=4 span[header_field]="Host"
+off=25 header_field complete
+off=26 len=11 span[header_value]="example.com"
+off=39 header_value complete
+off=39 len=10 span[header_field]="Connection"
+off=50 header_field complete
+off=51 len=10 span[header_value]="Something,"
+off=63 len=21 span[header_value]=" Upgrade, ,Keep-Alive"
+off=86 header_value complete
+off=86 len=18 span[header_field]="Sec-WebSocket-Key2"
+off=105 header_field complete
+off=106 len=18 span[header_value]="12998 5 Y3 1 .P00"
+off=126 header_value complete
+off=126 len=22 span[header_field]="Sec-WebSocket-Protocol"
+off=149 header_field complete
+off=150 len=6 span[header_value]="sample"
+off=158 header_value complete
+off=158 len=7 span[header_field]="Upgrade"
+off=166 header_field complete
+off=167 len=9 span[header_value]="WebSocket"
+off=178 header_value complete
+off=178 len=18 span[header_field]="Sec-WebSocket-Key1"
+off=197 header_field complete
+off=198 len=20 span[header_value]="4 @1 46546xW%0l 1 5"
+off=220 header_value complete
+off=220 len=6 span[header_field]="Origin"
+off=227 header_field complete
+off=228 len=18 span[header_value]="http://example.com"
+off=248 header_value complete
+off=250 headers complete method=1 v=1/1 flags=15 content_length=0
+off=250 message complete
+off=250 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### Multiple tokens with folding and LWS
+
+<!-- meta={"type": "request"} -->
+```http
+GET /demo HTTP/1.1
+Connection: keep-alive, upgrade
+Upgrade: WebSocket
+
+Hot diggity dogg
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=5 span[url]="/demo"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=20 len=10 span[header_field]="Connection"
+off=31 header_field complete
+off=32 len=19 span[header_value]="keep-alive, upgrade"
+off=53 header_value complete
+off=53 len=7 span[header_field]="Upgrade"
+off=61 header_field complete
+off=62 len=9 span[header_value]="WebSocket"
+off=73 header_value complete
+off=75 headers complete method=1 v=1/1 flags=15 content_length=0
+off=75 message complete
+off=75 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### Multiple tokens with folding, LWS, and CRLF
+
+<!-- meta={"type": "request"} -->
+```http
+GET /demo HTTP/1.1
+Connection: keep-alive, \r\n upgrade
+Upgrade: WebSocket
+
+Hot diggity dogg
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=5 span[url]="/demo"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=20 len=10 span[header_field]="Connection"
+off=31 header_field complete
+off=32 len=12 span[header_value]="keep-alive, "
+off=46 len=8 span[header_value]=" upgrade"
+off=56 header_value complete
+off=56 len=7 span[header_field]="Upgrade"
+off=64 header_field complete
+off=65 len=9 span[header_value]="WebSocket"
+off=76 header_value complete
+off=78 headers complete method=1 v=1/1 flags=15 content_length=0
+off=78 message complete
+off=78 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### Invalid whitespace token with `Connection` header field
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection : upgrade
+Content-Length: 4
+Upgrade: ws
+
+abcdefgh
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 error code=10 reason="Invalid header field char"
+```
+
+### Invalid whitespace token with `Connection` header field (lenient)
+
+<!-- meta={"type": "request-lenient-headers"} -->
+```http
+PUT /url HTTP/1.1
+Connection : upgrade
+Content-Length: 4
+Upgrade: ws
+
+abcdefgh
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=11 span[header_field]="Connection "
+off=31 header_field complete
+off=32 len=7 span[header_value]="upgrade"
+off=41 header_value complete
+off=41 len=14 span[header_field]="Content-Length"
+off=56 header_field complete
+off=57 len=1 span[header_value]="4"
+off=60 header_value complete
+off=60 len=7 span[header_field]="Upgrade"
+off=68 header_field complete
+off=69 len=2 span[header_value]="ws"
+off=73 header_value complete
+off=75 headers complete method=4 v=1/1 flags=34 content_length=4
+off=75 len=4 span[body]="abcd"
+off=79 message complete
+off=79 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+## `upgrade`
+
+### Setting a flag and pausing
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: upgrade
+Upgrade: ws
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=7 span[header_value]="upgrade"
+off=40 header_value complete
+off=40 len=7 span[header_field]="Upgrade"
+off=48 header_field complete
+off=49 len=2 span[header_value]="ws"
+off=53 header_value complete
+off=55 headers complete method=4 v=1/1 flags=14 content_length=0
+off=55 message complete
+off=55 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### Emitting part of body and pausing
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: upgrade
+Content-Length: 4
+Upgrade: ws
+
+abcdefgh
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=7 span[header_value]="upgrade"
+off=40 header_value complete
+off=40 len=14 span[header_field]="Content-Length"
+off=55 header_field complete
+off=56 len=1 span[header_value]="4"
+off=59 header_value complete
+off=59 len=7 span[header_field]="Upgrade"
+off=67 header_field complete
+off=68 len=2 span[header_value]="ws"
+off=72 header_value complete
+off=74 headers complete method=4 v=1/1 flags=34 content_length=4
+off=74 len=4 span[body]="abcd"
+off=78 message complete
+off=78 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### Upgrade GET request
+
+<!-- meta={"type": "request"} -->
+```http
+GET /demo HTTP/1.1
+Host: example.com
+Connection: Upgrade
+Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
+Sec-WebSocket-Protocol: sample
+Upgrade: WebSocket
+Sec-WebSocket-Key1: 4 @1 46546xW%0l 1 5
+Origin: http://example.com
+
+Hot diggity dogg
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=5 span[url]="/demo"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=20 len=4 span[header_field]="Host"
+off=25 header_field complete
+off=26 len=11 span[header_value]="example.com"
+off=39 header_value complete
+off=39 len=10 span[header_field]="Connection"
+off=50 header_field complete
+off=51 len=7 span[header_value]="Upgrade"
+off=60 header_value complete
+off=60 len=18 span[header_field]="Sec-WebSocket-Key2"
+off=79 header_field complete
+off=80 len=18 span[header_value]="12998 5 Y3 1 .P00"
+off=100 header_value complete
+off=100 len=22 span[header_field]="Sec-WebSocket-Protocol"
+off=123 header_field complete
+off=124 len=6 span[header_value]="sample"
+off=132 header_value complete
+off=132 len=7 span[header_field]="Upgrade"
+off=140 header_field complete
+off=141 len=9 span[header_value]="WebSocket"
+off=152 header_value complete
+off=152 len=18 span[header_field]="Sec-WebSocket-Key1"
+off=171 header_field complete
+off=172 len=20 span[header_value]="4 @1 46546xW%0l 1 5"
+off=194 header_value complete
+off=194 len=6 span[header_field]="Origin"
+off=201 header_field complete
+off=202 len=18 span[header_value]="http://example.com"
+off=222 header_value complete
+off=224 headers complete method=1 v=1/1 flags=14 content_length=0
+off=224 message complete
+off=224 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### Upgrade POST request
+
+<!-- meta={"type": "request"} -->
+```http
+POST /demo HTTP/1.1
+Host: example.com
+Connection: Upgrade
+Upgrade: HTTP/2.0
+Content-Length: 15
+
+sweet post body\
+Hot diggity dogg
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=5 span[url]="/demo"
+off=11 url complete
+off=16 len=3 span[version]="1.1"
+off=19 version complete
+off=21 len=4 span[header_field]="Host"
+off=26 header_field complete
+off=27 len=11 span[header_value]="example.com"
+off=40 header_value complete
+off=40 len=10 span[header_field]="Connection"
+off=51 header_field complete
+off=52 len=7 span[header_value]="Upgrade"
+off=61 header_value complete
+off=61 len=7 span[header_field]="Upgrade"
+off=69 header_field complete
+off=70 len=8 span[header_value]="HTTP/2.0"
+off=80 header_value complete
+off=80 len=14 span[header_field]="Content-Length"
+off=95 header_field complete
+off=96 len=2 span[header_value]="15"
+off=100 header_value complete
+off=102 headers complete method=3 v=1/1 flags=34 content_length=15
+off=102 len=15 span[body]="sweet post body"
+off=117 message complete
+off=117 error code=22 reason="Pause on CONNECT/Upgrade"
+```
diff --git a/llhttp/test/request/content-length.md b/llhttp/test/request/content-length.md
new file mode 100644
index 0000000..524d183
--- /dev/null
+++ b/llhttp/test/request/content-length.md
@@ -0,0 +1,482 @@
+Content-Length header
+=====================
+
+## `Content-Length` with zeroes
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 003
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=3 span[header_value]="003"
+off=40 header_value complete
+off=42 headers complete method=4 v=1/1 flags=20 content_length=3
+off=42 len=3 span[body]="abc"
+off=45 message complete
+```
+
+## `Content-Length` with follow-up headers
+
+The way the parser works is that special headers (like `Content-Length`) first
+set `header_state` to appropriate value, and then apply custom parsing using
+that value. For `Content-Length`, in particular, the `header_state` is used for
+setting the flag too.
+
+Make sure that `header_state` is reset to `0`, so that the flag won't be
+attempted to set twice (and error).
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 003
+Ohai: world
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=3 span[header_value]="003"
+off=40 header_value complete
+off=40 len=4 span[header_field]="Ohai"
+off=45 header_field complete
+off=46 len=5 span[header_value]="world"
+off=53 header_value complete
+off=55 headers complete method=4 v=1/1 flags=20 content_length=3
+off=55 len=3 span[body]="abc"
+off=58 message complete
+```
+
+## Error on `Content-Length` overflow
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 1000000000000000000000
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=21 span[header_value]="100000000000000000000"
+off=56 error code=11 reason="Content-Length overflow"
+```
+
+## Error on duplicate `Content-Length`
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 1
+Content-Length: 2
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=1 span[header_value]="1"
+off=38 header_value complete
+off=38 len=14 span[header_field]="Content-Length"
+off=53 header_field complete
+off=54 error code=4 reason="Duplicate Content-Length"
+```
+
+## Error on simultaneous `Content-Length` and `Transfer-Encoding: identity`
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 1
+Transfer-Encoding: identity
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=1 span[header_value]="1"
+off=38 header_value complete
+off=38 len=17 span[header_field]="Transfer-Encoding"
+off=56 header_field complete
+off=56 error code=15 reason="Transfer-Encoding can't be present with Content-Length"
+```
+
+## Invalid whitespace token with `Content-Length` header field
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Connection: upgrade
+Content-Length : 4
+Upgrade: ws
+
+abcdefgh
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=7 span[header_value]="upgrade"
+off=40 header_value complete
+off=40 len=14 span[header_field]="Content-Length"
+off=55 error code=10 reason="Invalid header field char"
+```
+
+## Invalid whitespace token with `Content-Length` header field (lenient)
+
+<!-- meta={"type": "request-lenient-headers"} -->
+```http
+PUT /url HTTP/1.1
+Connection: upgrade
+Content-Length : 4
+Upgrade: ws
+
+abcdefgh
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=10 span[header_field]="Connection"
+off=30 header_field complete
+off=31 len=7 span[header_value]="upgrade"
+off=40 header_value complete
+off=40 len=15 span[header_field]="Content-Length "
+off=56 header_field complete
+off=57 len=1 span[header_value]="4"
+off=60 header_value complete
+off=60 len=7 span[header_field]="Upgrade"
+off=68 header_field complete
+off=69 len=2 span[header_value]="ws"
+off=73 header_value complete
+off=75 headers complete method=4 v=1/1 flags=34 content_length=4
+off=75 len=4 span[body]="abcd"
+off=79 message complete
+off=79 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+## No error on simultaneous `Content-Length` and `Transfer-Encoding: identity` (lenient)
+
+<!-- meta={"type": "request-lenient-chunked-length"} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 1
+Transfer-Encoding: identity
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=1 span[header_value]="1"
+off=38 header_value complete
+off=38 len=17 span[header_field]="Transfer-Encoding"
+off=56 header_field complete
+off=57 len=8 span[header_value]="identity"
+off=67 header_value complete
+off=69 headers complete method=4 v=1/1 flags=220 content_length=1
+```
+
+## Funky `Content-Length` with body
+
+<!-- meta={"type": "request"} -->
+```http
+GET /get_funky_content_length_body_hello HTTP/1.0
+conTENT-Length: 5
+
+HELLO
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=36 span[url]="/get_funky_content_length_body_hello"
+off=41 url complete
+off=46 len=3 span[version]="1.0"
+off=49 version complete
+off=51 len=14 span[header_field]="conTENT-Length"
+off=66 header_field complete
+off=67 len=1 span[header_value]="5"
+off=70 header_value complete
+off=72 headers complete method=1 v=1/0 flags=20 content_length=5
+off=72 len=5 span[body]="HELLO"
+off=77 message complete
+```
+
+## Spaces in `Content-Length` (surrounding)
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 42
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=34 len=3 span[header_value]="42 "
+off=39 header_value complete
+off=41 headers complete method=3 v=1/1 flags=20 content_length=42
+```
+
+### Spaces in `Content-Length` #2
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 4 2
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=2 span[header_value]="4 "
+off=35 error code=11 reason="Invalid character in Content-Length"
+```
+
+### Spaces in `Content-Length` #3
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 13 37
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=3 span[header_value]="13 "
+off=36 error code=11 reason="Invalid character in Content-Length"
+```
+
+### Empty `Content-Length`
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Content-Length:
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=34 error code=11 reason="Empty Content-Length"
+```
+
+## `Content-Length` with CR instead of dash
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+PUT /url HTTP/1.1
+Content\rLength: 003
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=26 error code=10 reason="Invalid header token"
+```
+
+## Content-Length reset when no body is received
+
+<!-- meta={"type": "request", "skipBody": true} -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 123
+
+POST /url HTTP/1.1
+Content-Length: 456
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=3 span[header_value]="123"
+off=40 header_value complete
+off=42 headers complete method=4 v=1/1 flags=20 content_length=123
+off=42 skip body
+off=42 message complete
+off=42 reset
+off=42 message begin
+off=42 len=4 span[method]="POST"
+off=46 method complete
+off=47 len=4 span[url]="/url"
+off=52 url complete
+off=57 len=3 span[version]="1.1"
+off=60 version complete
+off=62 len=14 span[header_field]="Content-Length"
+off=77 header_field complete
+off=78 len=3 span[header_value]="456"
+off=83 header_value complete
+off=85 headers complete method=3 v=1/1 flags=20 content_length=456
+off=85 skip body
+off=85 message complete
+```
+
+## Missing CRLF-CRLF before body
+
+<!-- meta={"type": "request" } -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 3
+\rabc
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=1 span[header_value]="3"
+off=38 header_value complete
+off=39 error code=2 reason="Expected LF after headers"
+```
+
+## Missing CRLF-CRLF before body (lenient)
+
+<!-- meta={"type": "request-lenient-optional-lf-after-cr" } -->
+```http
+PUT /url HTTP/1.1
+Content-Length: 3
+\rabc
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=14 span[header_field]="Content-Length"
+off=34 header_field complete
+off=35 len=1 span[header_value]="3"
+off=38 header_value complete
+off=39 headers complete method=4 v=1/1 flags=20 content_length=3
+off=39 len=3 span[body]="abc"
+off=42 message complete
+``` \ No newline at end of file
diff --git a/llhttp/test/request/finish.md b/llhttp/test/request/finish.md
new file mode 100644
index 0000000..710daa5
--- /dev/null
+++ b/llhttp/test/request/finish.md
@@ -0,0 +1,69 @@
+Finish
+======
+
+Those tests check the return codes and the behavior of `llhttp_finish()` C API.
+
+## It should be safe to finish after GET request
+
+<!-- meta={"type": "request-finish"} -->
+```http
+GET / HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=18 headers complete method=1 v=1/1 flags=0 content_length=0
+off=18 message complete
+off=NULL finish=0
+```
+
+## It should be unsafe to finish after incomplete PUT request
+
+<!-- meta={"type": "request-finish"} -->
+```http
+PUT / HTTP/1.1
+Content-Length: 100
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=14 span[header_field]="Content-Length"
+off=31 header_field complete
+off=32 len=3 span[header_value]="100"
+off=NULL finish=2
+```
+
+## It should be unsafe to finish inside of the header
+
+<!-- meta={"type": "request-finish"} -->
+```http
+PUT / HTTP/1.1
+Content-Leng
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=12 span[header_field]="Content-Leng"
+off=NULL finish=2
+```
diff --git a/llhttp/test/request/invalid.md b/llhttp/test/request/invalid.md
new file mode 100644
index 0000000..9fb8383
--- /dev/null
+++ b/llhttp/test/request/invalid.md
@@ -0,0 +1,607 @@
+Invalid requests
+================
+
+### ICE protocol and GET method
+
+<!-- meta={"type": "request"} -->
+```http
+GET /music/sweet/music ICE/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=18 span[url]="/music/sweet/music"
+off=23 url complete
+off=27 error code=8 reason="Expected SOURCE method for ICE/x.x request"
+```
+
+### ICE protocol, but not really
+
+<!-- meta={"type": "request"} -->
+```http
+GET /music/sweet/music IHTTP/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=18 span[url]="/music/sweet/music"
+off=23 url complete
+off=24 error code=8 reason="Expected HTTP/"
+```
+
+### RTSP protocol and PUT method
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /music/sweet/music RTSP/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=18 span[url]="/music/sweet/music"
+off=23 url complete
+off=28 error code=8 reason="Invalid method for RTSP/x.x request"
+```
+
+### HTTP protocol and ANNOUNCE method
+
+<!-- meta={"type": "request"} -->
+```http
+ANNOUNCE /music/sweet/music HTTP/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=8 span[method]="ANNOUNCE"
+off=8 method complete
+off=9 len=18 span[url]="/music/sweet/music"
+off=28 url complete
+off=33 error code=8 reason="Invalid method for HTTP/x.x request"
+```
+
+### Headers separated by CR
+
+<!-- meta={"type": "request"} -->
+```http
+GET / HTTP/1.1
+Foo: 1\rBar: 2
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=3 span[header_field]="Foo"
+off=20 header_field complete
+off=21 len=1 span[header_value]="1"
+off=23 error code=3 reason="Missing expected LF after header value"
+```
+
+### Headers separated by LF
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Host: localhost:5000
+x:x\nTransfer-Encoding: chunked
+
+1
+A
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=14 span[header_value]="localhost:5000"
+off=39 header_value complete
+off=39 len=1 span[header_field]="x"
+off=41 header_field complete
+off=41 len=1 span[header_value]="x"
+off=42 error code=25 reason="Missing expected CR after header value"
+```
+
+### Headers separated by dummy characters
+
+<!-- meta={"type": "request"} -->
+```http
+GET / HTTP/1.1
+Connection: close
+Host: a
+\rZGET /evil: HTTP/1.1
+Host: a
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=10 span[header_field]="Connection"
+off=27 header_field complete
+off=28 len=5 span[header_value]="close"
+off=35 header_value complete
+off=35 len=4 span[header_field]="Host"
+off=40 header_field complete
+off=41 len=1 span[header_value]="a"
+off=44 header_value complete
+off=45 error code=2 reason="Expected LF after headers"
+```
+
+
+### Headers separated by dummy characters (lenient)
+
+<!-- meta={"type": "request-lenient-optional-lf-after-cr"} -->
+```http
+GET / HTTP/1.1
+Connection: close
+Host: a
+\rZGET /evil: HTTP/1.1
+Host: a
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=10 span[header_field]="Connection"
+off=27 header_field complete
+off=28 len=5 span[header_value]="close"
+off=35 header_value complete
+off=35 len=4 span[header_field]="Host"
+off=40 header_field complete
+off=41 len=1 span[header_value]="a"
+off=44 header_value complete
+off=45 headers complete method=1 v=1/1 flags=2 content_length=0
+off=45 message complete
+off=46 error code=5 reason="Data after `Connection: close`"
+```
+
+### Empty headers separated by CR
+
+<!-- meta={"type": "request" } -->
+```http
+POST / HTTP/1.1
+Connection: Close
+Host: localhost:5000
+x:\rTransfer-Encoding: chunked
+
+1
+A
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=10 span[header_field]="Connection"
+off=28 header_field complete
+off=29 len=5 span[header_value]="Close"
+off=36 header_value complete
+off=36 len=4 span[header_field]="Host"
+off=41 header_field complete
+off=42 len=14 span[header_value]="localhost:5000"
+off=58 header_value complete
+off=58 len=1 span[header_field]="x"
+off=60 header_field complete
+off=61 error code=2 reason="Expected LF after CR"
+```
+
+### Empty headers separated by LF
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Host: localhost:5000
+x:\nTransfer-Encoding: chunked
+
+1
+A
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=14 span[header_value]="localhost:5000"
+off=39 header_value complete
+off=39 len=1 span[header_field]="x"
+off=41 header_field complete
+off=42 error code=10 reason="Invalid header value char"
+```
+
+### Invalid header token #1
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+Fo@: Failure
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=18 error code=10 reason="Invalid header token"
+```
+
+### Invalid header token #2
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+Foo\01\test: Bar
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=19 error code=10 reason="Invalid header token"
+```
+
+### Invalid header token #3
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+: Bar
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 error code=10 reason="Invalid header token"
+```
+
+### Invalid method
+
+<!-- meta={"type": "request"} -->
+```http
+MKCOLA / HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=5 span[method]="MKCOL"
+off=5 method complete
+off=5 error code=6 reason="Expected space after method"
+```
+
+### Illegal header field name line folding
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+name
+ : value
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=20 error code=10 reason="Invalid header token"
+```
+
+### Corrupted Connection header
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+Host: www.example.com
+Connection\r\033\065\325eep-Alive
+Accept-Encoding: gzip
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=15 span[header_value]="www.example.com"
+off=39 header_value complete
+off=49 error code=10 reason="Invalid header token"
+```
+
+### Corrupted header name
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+Host: www.example.com
+X-Some-Header\r\033\065\325eep-Alive
+Accept-Encoding: gzip
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=15 span[header_value]="www.example.com"
+off=39 header_value complete
+off=52 error code=10 reason="Invalid header token"
+```
+
+### Missing CR between headers
+
+<!-- meta={"type": "request", "noScan": true} -->
+
+```http
+GET / HTTP/1.1
+Host: localhost
+Dummy: x\nContent-Length: 23
+
+GET / HTTP/1.1
+Dummy: GET /admin HTTP/1.1
+Host: localhost
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=9 span[header_value]="localhost"
+off=33 header_value complete
+off=33 len=5 span[header_field]="Dummy"
+off=39 header_field complete
+off=40 len=1 span[header_value]="x"
+off=41 error code=25 reason="Missing expected CR after header value"
+```
+
+### Invalid HTTP version
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/5.6
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="5.6"
+off=14 error code=9 reason="Invalid HTTP version"
+```
+
+## Invalid space after start line
+
+<!-- meta={"type": "request"} -->
+```http
+GET / HTTP/1.1
+ Host: foo
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=17 error code=30 reason="Unexpected space after start line"
+```
+
+
+### Only LFs present
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1\n\
+Transfer-Encoding: chunked\n\
+Trailer: Baz
+Foo: abc\n\
+Bar: def\n\
+\n\
+1\n\
+A\n\
+1;abc\n\
+B\n\
+1;def=ghi\n\
+C\n\
+1;jkl="mno"\n\
+D\n\
+0\n\
+\n\
+Baz: ghi\n\
+\n\
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=16 error code=9 reason="Expected CRLF after version"
+```
+
+### Only LFs present (lenient)
+
+<!-- meta={"type": "request-lenient-all"} -->
+```http
+POST / HTTP/1.1\n\
+Transfer-Encoding: chunked\n\
+Trailer: Baz
+Foo: abc\n\
+Bar: def\n\
+\n\
+1\n\
+A\n\
+1;abc\n\
+B\n\
+1;def=ghi\n\
+C\n\
+1;jkl="mno"\n\
+D\n\
+0\n\
+\n\
+Baz: ghi\n\
+\n
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=16 len=17 span[header_field]="Transfer-Encoding"
+off=34 header_field complete
+off=35 len=7 span[header_value]="chunked"
+off=43 header_value complete
+off=43 len=7 span[header_field]="Trailer"
+off=51 header_field complete
+off=52 len=3 span[header_value]="Baz"
+off=57 header_value complete
+off=57 len=3 span[header_field]="Foo"
+off=61 header_field complete
+off=62 len=3 span[header_value]="abc"
+off=66 header_value complete
+off=66 len=3 span[header_field]="Bar"
+off=70 header_field complete
+off=71 len=3 span[header_value]="def"
+off=75 header_value complete
+off=76 headers complete method=3 v=1/1 flags=208 content_length=0
+off=78 chunk header len=1
+off=78 len=1 span[body]="A"
+off=80 chunk complete
+off=82 len=3 span[chunk_extension_name]="abc"
+off=85 chunk_extension_name complete
+off=86 chunk header len=1
+off=86 len=1 span[body]="B"
+off=88 chunk complete
+off=90 len=3 span[chunk_extension_name]="def"
+off=94 chunk_extension_name complete
+off=94 len=3 span[chunk_extension_value]="ghi"
+off=97 chunk_extension_value complete
+off=98 chunk header len=1
+off=98 len=1 span[body]="C"
+off=100 chunk complete
+off=102 len=3 span[chunk_extension_name]="jkl"
+off=106 chunk_extension_name complete
+off=106 len=5 span[chunk_extension_value]=""mno""
+off=111 chunk_extension_value complete
+off=112 chunk header len=1
+off=112 len=1 span[body]="D"
+off=114 chunk complete
+off=117 chunk header len=0
+off=117 len=3 span[header_field]="Baz"
+off=121 header_field complete
+off=122 len=3 span[header_value]="ghi"
+off=126 header_value complete
+off=127 chunk complete
+off=127 message complete
+``` \ No newline at end of file
diff --git a/llhttp/test/request/lenient-headers.md b/llhttp/test/request/lenient-headers.md
new file mode 100644
index 0000000..05e105f
--- /dev/null
+++ b/llhttp/test/request/lenient-headers.md
@@ -0,0 +1,145 @@
+Lenient header value parsing
+============================
+
+Parsing with header value token checks off.
+
+## Header value (lenient)
+
+<!-- meta={"type": "request-lenient-headers"} -->
+```http
+GET /url HTTP/1.1
+Header1: \f
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=7 span[header_field]="Header1"
+off=27 header_field complete
+off=28 len=1 span[header_value]="\f"
+off=31 header_value complete
+off=33 headers complete method=1 v=1/1 flags=0 content_length=0
+off=33 message complete
+```
+
+## Second request header value (lenient)
+
+<!-- meta={"type": "request-lenient-headers"} -->
+```http
+GET /url HTTP/1.1
+Header1: Okay
+
+
+GET /url HTTP/1.1
+Header1: \f
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=7 span[header_field]="Header1"
+off=27 header_field complete
+off=28 len=4 span[header_value]="Okay"
+off=34 header_value complete
+off=36 headers complete method=1 v=1/1 flags=0 content_length=0
+off=36 message complete
+off=38 reset
+off=38 message begin
+off=38 len=3 span[method]="GET"
+off=41 method complete
+off=42 len=4 span[url]="/url"
+off=47 url complete
+off=52 len=3 span[version]="1.1"
+off=55 version complete
+off=57 len=7 span[header_field]="Header1"
+off=65 header_field complete
+off=66 len=1 span[header_value]="\f"
+off=69 header_value complete
+off=71 headers complete method=1 v=1/1 flags=0 content_length=0
+off=71 message complete
+```
+
+## Header value
+
+<!-- meta={"type": "request"} -->
+```http
+GET /url HTTP/1.1
+Header1: \f
+
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=7 span[header_field]="Header1"
+off=27 header_field complete
+off=28 len=0 span[header_value]=""
+off=28 error code=10 reason="Invalid header value char"
+```
+
+### Empty headers separated by CR (lenient)
+
+<!-- meta={"type": "request-lenient-headers"} -->
+```http
+POST / HTTP/1.1
+Connection: Close
+Host: localhost:5000
+x:\rTransfer-Encoding: chunked
+
+1
+A
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=10 span[header_field]="Connection"
+off=28 header_field complete
+off=29 len=5 span[header_value]="Close"
+off=36 header_value complete
+off=36 len=4 span[header_field]="Host"
+off=41 header_field complete
+off=42 len=14 span[header_value]="localhost:5000"
+off=58 header_value complete
+off=58 len=1 span[header_field]="x"
+off=60 header_field complete
+off=61 len=0 span[header_value]=""
+off=61 header_value complete
+off=61 len=17 span[header_field]="Transfer-Encoding"
+off=79 header_field complete
+off=80 len=7 span[header_value]="chunked"
+off=89 header_value complete
+off=91 headers complete method=3 v=1/1 flags=20a content_length=0
+off=94 chunk header len=1
+off=94 len=1 span[body]="A"
+off=97 chunk complete
+off=100 chunk header len=0
+``` \ No newline at end of file
diff --git a/llhttp/test/request/lenient-version.md b/llhttp/test/request/lenient-version.md
new file mode 100644
index 0000000..4185556
--- /dev/null
+++ b/llhttp/test/request/lenient-version.md
@@ -0,0 +1,23 @@
+Lenient HTTP version parsing
+============================
+
+### Invalid HTTP version (lenient)
+
+<!-- meta={"type": "request-lenient-version"} -->
+```http
+GET / HTTP/5.6
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="5.6"
+off=14 version complete
+off=18 headers complete method=1 v=5/6 flags=0 content_length=0
+off=18 message complete
+``` \ No newline at end of file
diff --git a/llhttp/test/request/method.md b/llhttp/test/request/method.md
new file mode 100644
index 0000000..dce262e
--- /dev/null
+++ b/llhttp/test/request/method.md
@@ -0,0 +1,450 @@
+Methods
+=======
+
+### REPORT request
+
+<!-- meta={"type": "request"} -->
+```http
+REPORT /test HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=6 span[method]="REPORT"
+off=6 method complete
+off=7 len=5 span[url]="/test"
+off=13 url complete
+off=18 len=3 span[version]="1.1"
+off=21 version complete
+off=25 headers complete method=20 v=1/1 flags=0 content_length=0
+off=25 message complete
+```
+
+### CONNECT request
+
+<!-- meta={"type": "request"} -->
+```http
+CONNECT 0-home0.netscape.com:443 HTTP/1.0
+User-agent: Mozilla/1.1N
+Proxy-authorization: basic aGVsbG86d29ybGQ=
+
+some data
+and yet even more data
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="CONNECT"
+off=7 method complete
+off=8 len=24 span[url]="0-home0.netscape.com:443"
+off=33 url complete
+off=38 len=3 span[version]="1.0"
+off=41 version complete
+off=43 len=10 span[header_field]="User-agent"
+off=54 header_field complete
+off=55 len=12 span[header_value]="Mozilla/1.1N"
+off=69 header_value complete
+off=69 len=19 span[header_field]="Proxy-authorization"
+off=89 header_field complete
+off=90 len=22 span[header_value]="basic aGVsbG86d29ybGQ="
+off=114 header_value complete
+off=116 headers complete method=5 v=1/0 flags=0 content_length=0
+off=116 message complete
+off=116 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### CONNECT request with CAPS
+
+<!-- meta={"type": "request"} -->
+```http
+CONNECT HOME0.NETSCAPE.COM:443 HTTP/1.0
+User-agent: Mozilla/1.1N
+Proxy-authorization: basic aGVsbG86d29ybGQ=
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="CONNECT"
+off=7 method complete
+off=8 len=22 span[url]="HOME0.NETSCAPE.COM:443"
+off=31 url complete
+off=36 len=3 span[version]="1.0"
+off=39 version complete
+off=41 len=10 span[header_field]="User-agent"
+off=52 header_field complete
+off=53 len=12 span[header_value]="Mozilla/1.1N"
+off=67 header_value complete
+off=67 len=19 span[header_field]="Proxy-authorization"
+off=87 header_field complete
+off=88 len=22 span[header_value]="basic aGVsbG86d29ybGQ="
+off=112 header_value complete
+off=114 headers complete method=5 v=1/0 flags=0 content_length=0
+off=114 message complete
+off=114 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### CONNECT with body
+
+<!-- meta={"type": "request"} -->
+```http
+CONNECT foo.bar.com:443 HTTP/1.0
+User-agent: Mozilla/1.1N
+Proxy-authorization: basic aGVsbG86d29ybGQ=
+Content-Length: 10
+
+blarfcicle"
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="CONNECT"
+off=7 method complete
+off=8 len=15 span[url]="foo.bar.com:443"
+off=24 url complete
+off=29 len=3 span[version]="1.0"
+off=32 version complete
+off=34 len=10 span[header_field]="User-agent"
+off=45 header_field complete
+off=46 len=12 span[header_value]="Mozilla/1.1N"
+off=60 header_value complete
+off=60 len=19 span[header_field]="Proxy-authorization"
+off=80 header_field complete
+off=81 len=22 span[header_value]="basic aGVsbG86d29ybGQ="
+off=105 header_value complete
+off=105 len=14 span[header_field]="Content-Length"
+off=120 header_field complete
+off=121 len=2 span[header_value]="10"
+off=125 header_value complete
+off=127 headers complete method=5 v=1/0 flags=20 content_length=10
+off=127 message complete
+off=127 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+### M-SEARCH request
+
+<!-- meta={"type": "request"} -->
+```http
+M-SEARCH * HTTP/1.1
+HOST: 239.255.255.250:1900
+MAN: "ssdp:discover"
+ST: "ssdp:all"
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=8 span[method]="M-SEARCH"
+off=8 method complete
+off=9 len=1 span[url]="*"
+off=11 url complete
+off=16 len=3 span[version]="1.1"
+off=19 version complete
+off=21 len=4 span[header_field]="HOST"
+off=26 header_field complete
+off=27 len=20 span[header_value]="239.255.255.250:1900"
+off=49 header_value complete
+off=49 len=3 span[header_field]="MAN"
+off=53 header_field complete
+off=54 len=15 span[header_value]=""ssdp:discover""
+off=71 header_value complete
+off=71 len=2 span[header_field]="ST"
+off=74 header_field complete
+off=75 len=10 span[header_value]=""ssdp:all""
+off=87 header_value complete
+off=89 headers complete method=24 v=1/1 flags=0 content_length=0
+off=89 message complete
+```
+
+### PATCH request
+
+<!-- meta={"type": "request"} -->
+```http
+PATCH /file.txt HTTP/1.1
+Host: www.example.com
+Content-Type: application/example
+If-Match: "e0023aa4e"
+Content-Length: 10
+
+cccccccccc
+```
+
+```log
+off=0 message begin
+off=0 len=5 span[method]="PATCH"
+off=5 method complete
+off=6 len=9 span[url]="/file.txt"
+off=16 url complete
+off=21 len=3 span[version]="1.1"
+off=24 version complete
+off=26 len=4 span[header_field]="Host"
+off=31 header_field complete
+off=32 len=15 span[header_value]="www.example.com"
+off=49 header_value complete
+off=49 len=12 span[header_field]="Content-Type"
+off=62 header_field complete
+off=63 len=19 span[header_value]="application/example"
+off=84 header_value complete
+off=84 len=8 span[header_field]="If-Match"
+off=93 header_field complete
+off=94 len=11 span[header_value]=""e0023aa4e""
+off=107 header_value complete
+off=107 len=14 span[header_field]="Content-Length"
+off=122 header_field complete
+off=123 len=2 span[header_value]="10"
+off=127 header_value complete
+off=129 headers complete method=28 v=1/1 flags=20 content_length=10
+off=129 len=10 span[body]="cccccccccc"
+off=139 message complete
+```
+
+### PURGE request
+
+<!-- meta={"type": "request"} -->
+```http
+PURGE /file.txt HTTP/1.1
+Host: www.example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=5 span[method]="PURGE"
+off=5 method complete
+off=6 len=9 span[url]="/file.txt"
+off=16 url complete
+off=21 len=3 span[version]="1.1"
+off=24 version complete
+off=26 len=4 span[header_field]="Host"
+off=31 header_field complete
+off=32 len=15 span[header_value]="www.example.com"
+off=49 header_value complete
+off=51 headers complete method=29 v=1/1 flags=0 content_length=0
+off=51 message complete
+```
+
+### SEARCH request
+
+<!-- meta={"type": "request"} -->
+```http
+SEARCH / HTTP/1.1
+Host: www.example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=6 span[method]="SEARCH"
+off=6 method complete
+off=7 len=1 span[url]="/"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=4 span[header_field]="Host"
+off=24 header_field complete
+off=25 len=15 span[header_value]="www.example.com"
+off=42 header_value complete
+off=44 headers complete method=14 v=1/1 flags=0 content_length=0
+off=44 message complete
+```
+
+### LINK request
+
+<!-- meta={"type": "request"} -->
+```http
+LINK /images/my_dog.jpg HTTP/1.1
+Host: example.com
+Link: <http://example.com/profiles/joe>; rel="tag"
+Link: <http://example.com/profiles/sally>; rel="tag"
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="LINK"
+off=4 method complete
+off=5 len=18 span[url]="/images/my_dog.jpg"
+off=24 url complete
+off=29 len=3 span[version]="1.1"
+off=32 version complete
+off=34 len=4 span[header_field]="Host"
+off=39 header_field complete
+off=40 len=11 span[header_value]="example.com"
+off=53 header_value complete
+off=53 len=4 span[header_field]="Link"
+off=58 header_field complete
+off=59 len=44 span[header_value]="<http://example.com/profiles/joe>; rel="tag""
+off=105 header_value complete
+off=105 len=4 span[header_field]="Link"
+off=110 header_field complete
+off=111 len=46 span[header_value]="<http://example.com/profiles/sally>; rel="tag""
+off=159 header_value complete
+off=161 headers complete method=31 v=1/1 flags=0 content_length=0
+off=161 message complete
+```
+
+### LINK request
+
+<!-- meta={"type": "request"} -->
+```http
+UNLINK /images/my_dog.jpg HTTP/1.1
+Host: example.com
+Link: <http://example.com/profiles/sally>; rel="tag"
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=6 span[method]="UNLINK"
+off=6 method complete
+off=7 len=18 span[url]="/images/my_dog.jpg"
+off=26 url complete
+off=31 len=3 span[version]="1.1"
+off=34 version complete
+off=36 len=4 span[header_field]="Host"
+off=41 header_field complete
+off=42 len=11 span[header_value]="example.com"
+off=55 header_value complete
+off=55 len=4 span[header_field]="Link"
+off=60 header_field complete
+off=61 len=46 span[header_value]="<http://example.com/profiles/sally>; rel="tag""
+off=109 header_value complete
+off=111 headers complete method=32 v=1/1 flags=0 content_length=0
+off=111 message complete
+```
+
+### SOURCE request
+
+<!-- meta={"type": "request"} -->
+```http
+SOURCE /music/sweet/music HTTP/1.1
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=6 span[method]="SOURCE"
+off=6 method complete
+off=7 len=18 span[url]="/music/sweet/music"
+off=26 url complete
+off=31 len=3 span[version]="1.1"
+off=34 version complete
+off=36 len=4 span[header_field]="Host"
+off=41 header_field complete
+off=42 len=11 span[header_value]="example.com"
+off=55 header_value complete
+off=57 headers complete method=33 v=1/1 flags=0 content_length=0
+off=57 message complete
+```
+
+### SOURCE request with ICE
+
+<!-- meta={"type": "request"} -->
+```http
+SOURCE /music/sweet/music ICE/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=6 span[method]="SOURCE"
+off=6 method complete
+off=7 len=18 span[url]="/music/sweet/music"
+off=26 url complete
+off=30 len=3 span[version]="1.0"
+off=33 version complete
+off=35 len=4 span[header_field]="Host"
+off=40 header_field complete
+off=41 len=11 span[header_value]="example.com"
+off=54 header_value complete
+off=56 headers complete method=33 v=1/0 flags=0 content_length=0
+off=56 message complete
+```
+
+### OPTIONS request with RTSP
+
+NOTE: `OPTIONS` is a valid HTTP metho too.
+
+<!-- meta={"type": "request"} -->
+```http
+OPTIONS /music/sweet/music RTSP/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="OPTIONS"
+off=7 method complete
+off=8 len=18 span[url]="/music/sweet/music"
+off=27 url complete
+off=32 len=3 span[version]="1.0"
+off=35 version complete
+off=37 len=4 span[header_field]="Host"
+off=42 header_field complete
+off=43 len=11 span[header_value]="example.com"
+off=56 header_value complete
+off=58 headers complete method=6 v=1/0 flags=0 content_length=0
+off=58 message complete
+```
+
+### ANNOUNCE request with RTSP
+
+<!-- meta={"type": "request"} -->
+```http
+ANNOUNCE /music/sweet/music RTSP/1.0
+Host: example.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=8 span[method]="ANNOUNCE"
+off=8 method complete
+off=9 len=18 span[url]="/music/sweet/music"
+off=28 url complete
+off=33 len=3 span[version]="1.0"
+off=36 version complete
+off=38 len=4 span[header_field]="Host"
+off=43 header_field complete
+off=44 len=11 span[header_value]="example.com"
+off=57 header_value complete
+off=59 headers complete method=36 v=1/0 flags=0 content_length=0
+off=59 message complete
+```
+
+### PRI request HTTP2
+
+<!-- meta={"type": "request"} -->
+```http
+PRI * HTTP/1.1
+
+SM
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PRI"
+off=3 method complete
+off=4 len=1 span[url]="*"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=24 error code=23 reason="Pause on PRI/Upgrade"
+```
diff --git a/llhttp/test/request/pausing.md b/llhttp/test/request/pausing.md
new file mode 100644
index 0000000..8e501e3
--- /dev/null
+++ b/llhttp/test/request/pausing.md
@@ -0,0 +1,381 @@
+Pausing
+=======
+
+### on_message_begin
+
+<!-- meta={"type": "request", "pause": "on_message_begin"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 pause
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_message_complete
+
+<!-- meta={"type": "request", "pause": "on_message_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+off=41 pause
+```
+
+### on_method_complete
+
+<!-- meta={"type": "request", "pause": "on_method_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=4 pause
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_url_complete
+
+<!-- meta={"type": "request", "pause": "on_url_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=7 pause
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_version_complete
+
+<!-- meta={"type": "request", "pause": "on_version_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=15 pause
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_header_field_complete
+
+<!-- meta={"type": "request", "pause": "on_header_field_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=32 pause
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_header_value_complete
+
+<!-- meta={"type": "request", "pause": "on_header_value_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=36 pause
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_headers_complete
+
+<!-- meta={"type": "request", "pause": "on_headers_complete"} -->
+```http
+POST / HTTP/1.1
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete method=3 v=1/1 flags=20 content_length=3
+off=38 pause
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_chunk_header
+
+<!-- meta={"type": "request", "pause": "on_chunk_header"} -->
+```http
+PUT / HTTP/1.1
+Transfer-Encoding: chunked
+
+a
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=17 span[header_field]="Transfer-Encoding"
+off=34 header_field complete
+off=35 len=7 span[header_value]="chunked"
+off=44 header_value complete
+off=46 headers complete method=4 v=1/1 flags=208 content_length=0
+off=49 chunk header len=10
+off=49 pause
+off=49 len=10 span[body]="0123456789"
+off=61 chunk complete
+off=64 chunk header len=0
+off=64 pause
+off=66 chunk complete
+off=66 message complete
+```
+
+### on_chunk_extension_name
+
+<!-- meta={"type": "request", "pause": "on_chunk_extension_name"} -->
+```http
+PUT / HTTP/1.1
+Transfer-Encoding: chunked
+
+a;foo=bar
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=17 span[header_field]="Transfer-Encoding"
+off=34 header_field complete
+off=35 len=7 span[header_value]="chunked"
+off=44 header_value complete
+off=46 headers complete method=4 v=1/1 flags=208 content_length=0
+off=48 len=3 span[chunk_extension_name]="foo"
+off=52 chunk_extension_name complete
+off=52 pause
+off=52 len=3 span[chunk_extension_value]="bar"
+off=56 chunk_extension_value complete
+off=57 chunk header len=10
+off=57 len=10 span[body]="0123456789"
+off=69 chunk complete
+off=72 chunk header len=0
+off=74 chunk complete
+off=74 message complete
+```
+
+### on_chunk_extension_value
+
+<!-- meta={"type": "request", "pause": "on_chunk_extension_value"} -->
+```http
+PUT / HTTP/1.1
+Transfer-Encoding: chunked
+
+a;foo=bar
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=17 span[header_field]="Transfer-Encoding"
+off=34 header_field complete
+off=35 len=7 span[header_value]="chunked"
+off=44 header_value complete
+off=46 headers complete method=4 v=1/1 flags=208 content_length=0
+off=48 len=3 span[chunk_extension_name]="foo"
+off=52 chunk_extension_name complete
+off=52 len=3 span[chunk_extension_value]="bar"
+off=56 chunk_extension_value complete
+off=56 pause
+off=57 chunk header len=10
+off=57 len=10 span[body]="0123456789"
+off=69 chunk complete
+off=72 chunk header len=0
+off=74 chunk complete
+off=74 message complete
+```
+
+
+### on_chunk_complete
+
+<!-- meta={"type": "request", "pause": "on_chunk_complete"} -->
+```http
+PUT / HTTP/1.1
+Transfer-Encoding: chunked
+
+a
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=17 span[header_field]="Transfer-Encoding"
+off=34 header_field complete
+off=35 len=7 span[header_value]="chunked"
+off=44 header_value complete
+off=46 headers complete method=4 v=1/1 flags=208 content_length=0
+off=49 chunk header len=10
+off=49 len=10 span[body]="0123456789"
+off=61 chunk complete
+off=61 pause
+off=64 chunk header len=0
+off=66 chunk complete
+off=66 pause
+off=66 message complete
+```
diff --git a/llhttp/test/request/pipelining.md b/llhttp/test/request/pipelining.md
new file mode 100644
index 0000000..bdfe6ab
--- /dev/null
+++ b/llhttp/test/request/pipelining.md
@@ -0,0 +1,66 @@
+Pipelining
+==========
+
+## Should parse multiple events
+
+<!-- meta={"type": "request"} -->
+```http
+POST /aaa HTTP/1.1
+Content-Length: 3
+
+AAA
+PUT /bbb HTTP/1.1
+Content-Length: 4
+
+BBBB
+PATCH /ccc HTTP/1.1
+Content-Length: 5
+
+CCCC
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=4 span[url]="/aaa"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=20 len=14 span[header_field]="Content-Length"
+off=35 header_field complete
+off=36 len=1 span[header_value]="3"
+off=39 header_value complete
+off=41 headers complete method=3 v=1/1 flags=20 content_length=3
+off=41 len=3 span[body]="AAA"
+off=44 message complete
+off=46 reset
+off=46 message begin
+off=46 len=3 span[method]="PUT"
+off=49 method complete
+off=50 len=4 span[url]="/bbb"
+off=55 url complete
+off=60 len=3 span[version]="1.1"
+off=63 version complete
+off=65 len=14 span[header_field]="Content-Length"
+off=80 header_field complete
+off=81 len=1 span[header_value]="4"
+off=84 header_value complete
+off=86 headers complete method=4 v=1/1 flags=20 content_length=4
+off=86 len=4 span[body]="BBBB"
+off=90 message complete
+off=92 reset
+off=92 message begin
+off=92 len=5 span[method]="PATCH"
+off=97 method complete
+off=98 len=4 span[url]="/ccc"
+off=103 url complete
+off=108 len=3 span[version]="1.1"
+off=111 version complete
+off=113 len=14 span[header_field]="Content-Length"
+off=128 header_field complete
+off=129 len=1 span[header_value]="5"
+off=132 header_value complete
+off=134 headers complete method=28 v=1/1 flags=20 content_length=5
+off=134 len=4 span[body]="CCCC"
+``` \ No newline at end of file
diff --git a/llhttp/test/request/sample.md b/llhttp/test/request/sample.md
new file mode 100644
index 0000000..f0a5d44
--- /dev/null
+++ b/llhttp/test/request/sample.md
@@ -0,0 +1,629 @@
+Sample requests
+===============
+
+Lots of sample requests, most ported from [http_parser][0] test suite.
+
+## Simple request
+
+<!-- meta={"type": "request"} -->
+```http
+OPTIONS /url HTTP/1.1
+Header1: Value1
+Header2:\t Value2
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="OPTIONS"
+off=7 method complete
+off=8 len=4 span[url]="/url"
+off=13 url complete
+off=18 len=3 span[version]="1.1"
+off=21 version complete
+off=23 len=7 span[header_field]="Header1"
+off=31 header_field complete
+off=32 len=6 span[header_value]="Value1"
+off=40 header_value complete
+off=40 len=7 span[header_field]="Header2"
+off=48 header_field complete
+off=50 len=6 span[header_value]="Value2"
+off=58 header_value complete
+off=60 headers complete method=6 v=1/1 flags=0 content_length=0
+off=60 message complete
+```
+
+## Request with method starting with `H`
+
+There's a optimization in `start_req_or_res` that passes execution to
+`start_req` when the first character is not `H` (because response must start
+with `HTTP/`). However, there're still methods like `HEAD` that should get
+to `start_req`. Verify that it still works after optimization.
+
+<!-- meta={"type": "request", "noScan": true } -->
+```http
+HEAD /url HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="HEAD"
+off=4 method complete
+off=5 len=4 span[url]="/url"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=22 headers complete method=2 v=1/1 flags=0 content_length=0
+off=22 message complete
+```
+
+## curl GET
+
+<!-- meta={"type": "request"} -->
+```http
+GET /test HTTP/1.1
+User-Agent: curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1
+Host: 0.0.0.0=5000
+Accept: */*
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=5 span[url]="/test"
+off=10 url complete
+off=15 len=3 span[version]="1.1"
+off=18 version complete
+off=20 len=10 span[header_field]="User-Agent"
+off=31 header_field complete
+off=32 len=85 span[header_value]="curl/7.18.0 (i486-pc-linux-gnu) libcurl/7.18.0 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.1"
+off=119 header_value complete
+off=119 len=4 span[header_field]="Host"
+off=124 header_field complete
+off=125 len=12 span[header_value]="0.0.0.0=5000"
+off=139 header_value complete
+off=139 len=6 span[header_field]="Accept"
+off=146 header_field complete
+off=147 len=3 span[header_value]="*/*"
+off=152 header_value complete
+off=154 headers complete method=1 v=1/1 flags=0 content_length=0
+off=154 message complete
+```
+
+## Firefox GET
+
+<!-- meta={"type": "request"} -->
+```http
+GET /favicon.ico HTTP/1.1
+Host: 0.0.0.0=5000
+User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008061015 Firefox/3.0
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Accept-Language: en-us,en;q=0.5
+Accept-Encoding: gzip,deflate
+Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
+Keep-Alive: 300
+Connection: keep-alive
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=12 span[url]="/favicon.ico"
+off=17 url complete
+off=22 len=3 span[version]="1.1"
+off=25 version complete
+off=27 len=4 span[header_field]="Host"
+off=32 header_field complete
+off=33 len=12 span[header_value]="0.0.0.0=5000"
+off=47 header_value complete
+off=47 len=10 span[header_field]="User-Agent"
+off=58 header_field complete
+off=59 len=76 span[header_value]="Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008061015 Firefox/3.0"
+off=137 header_value complete
+off=137 len=6 span[header_field]="Accept"
+off=144 header_field complete
+off=145 len=63 span[header_value]="text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
+off=210 header_value complete
+off=210 len=15 span[header_field]="Accept-Language"
+off=226 header_field complete
+off=227 len=14 span[header_value]="en-us,en;q=0.5"
+off=243 header_value complete
+off=243 len=15 span[header_field]="Accept-Encoding"
+off=259 header_field complete
+off=260 len=12 span[header_value]="gzip,deflate"
+off=274 header_value complete
+off=274 len=14 span[header_field]="Accept-Charset"
+off=289 header_field complete
+off=290 len=30 span[header_value]="ISO-8859-1,utf-8;q=0.7,*;q=0.7"
+off=322 header_value complete
+off=322 len=10 span[header_field]="Keep-Alive"
+off=333 header_field complete
+off=334 len=3 span[header_value]="300"
+off=339 header_value complete
+off=339 len=10 span[header_field]="Connection"
+off=350 header_field complete
+off=351 len=10 span[header_value]="keep-alive"
+off=363 header_value complete
+off=365 headers complete method=1 v=1/1 flags=1 content_length=0
+off=365 message complete
+```
+
+## DUMBPACK
+
+<!-- meta={"type": "request"} -->
+```http
+GET /dumbpack HTTP/1.1
+aaaaaaaaaaaaa:++++++++++
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=9 span[url]="/dumbpack"
+off=14 url complete
+off=19 len=3 span[version]="1.1"
+off=22 version complete
+off=24 len=13 span[header_field]="aaaaaaaaaaaaa"
+off=38 header_field complete
+off=38 len=10 span[header_value]="++++++++++"
+off=50 header_value complete
+off=52 headers complete method=1 v=1/1 flags=0 content_length=0
+off=52 message complete
+```
+
+## No headers and no body
+
+<!-- meta={"type": "request"} -->
+```http
+GET /get_no_headers_no_body/world HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=29 span[url]="/get_no_headers_no_body/world"
+off=34 url complete
+off=39 len=3 span[version]="1.1"
+off=42 version complete
+off=46 headers complete method=1 v=1/1 flags=0 content_length=0
+off=46 message complete
+```
+
+## One header and no body
+
+<!-- meta={"type": "request"} -->
+```http
+GET /get_one_header_no_body HTTP/1.1
+Accept: */*
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=23 span[url]="/get_one_header_no_body"
+off=28 url complete
+off=33 len=3 span[version]="1.1"
+off=36 version complete
+off=38 len=6 span[header_field]="Accept"
+off=45 header_field complete
+off=46 len=3 span[header_value]="*/*"
+off=51 header_value complete
+off=53 headers complete method=1 v=1/1 flags=0 content_length=0
+off=53 message complete
+```
+
+## Apache bench GET
+
+The server receiving this request SHOULD NOT wait for EOF to know that
+`Content-Length == 0`.
+
+<!-- meta={"type": "request"} -->
+```http
+GET /test HTTP/1.0
+Host: 0.0.0.0:5000
+User-Agent: ApacheBench/2.3
+Accept: */*
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=5 span[url]="/test"
+off=10 url complete
+off=15 len=3 span[version]="1.0"
+off=18 version complete
+off=20 len=4 span[header_field]="Host"
+off=25 header_field complete
+off=26 len=12 span[header_value]="0.0.0.0:5000"
+off=40 header_value complete
+off=40 len=10 span[header_field]="User-Agent"
+off=51 header_field complete
+off=52 len=15 span[header_value]="ApacheBench/2.3"
+off=69 header_value complete
+off=69 len=6 span[header_field]="Accept"
+off=76 header_field complete
+off=77 len=3 span[header_value]="*/*"
+off=82 header_value complete
+off=84 headers complete method=1 v=1/0 flags=0 content_length=0
+off=84 message complete
+```
+
+## Prefix newline
+
+Some clients, especially after a POST in a keep-alive connection,
+will send an extra CRLF before the next request.
+
+<!-- meta={"type": "request"} -->
+```http
+\r\nGET /test HTTP/1.1
+
+
+```
+
+```log
+off=2 message begin
+off=2 len=3 span[method]="GET"
+off=5 method complete
+off=6 len=5 span[url]="/test"
+off=12 url complete
+off=17 len=3 span[version]="1.1"
+off=20 version complete
+off=24 headers complete method=1 v=1/1 flags=0 content_length=0
+off=24 message complete
+```
+
+## No HTTP version
+
+<!-- meta={"type": "request"} -->
+```http
+GET /
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=7 url complete
+off=9 headers complete method=1 v=0/9 flags=0 content_length=0
+off=9 message complete
+```
+
+## Line folding in header value with CRLF
+
+<!-- meta={"type": "request-lenient-headers"} -->
+```http
+GET / HTTP/1.1
+Line1: abc
+\tdef
+ ghi
+\t\tjkl
+ mno
+\t \tqrs
+Line2: \t line2\t
+Line3:
+ line3
+Line4:
+
+Connection:
+ close
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=5 span[header_field]="Line1"
+off=22 header_field complete
+off=25 len=3 span[header_value]="abc"
+off=30 len=4 span[header_value]="\tdef"
+off=36 len=4 span[header_value]=" ghi"
+off=42 len=5 span[header_value]="\t\tjkl"
+off=49 len=6 span[header_value]=" mno "
+off=57 len=6 span[header_value]="\t \tqrs"
+off=65 header_value complete
+off=65 len=5 span[header_field]="Line2"
+off=71 header_field complete
+off=74 len=6 span[header_value]="line2\t"
+off=82 header_value complete
+off=82 len=5 span[header_field]="Line3"
+off=88 header_field complete
+off=91 len=5 span[header_value]="line3"
+off=98 header_value complete
+off=98 len=5 span[header_field]="Line4"
+off=104 header_field complete
+off=110 len=0 span[header_value]=""
+off=110 header_value complete
+off=110 len=10 span[header_field]="Connection"
+off=121 header_field complete
+off=124 len=5 span[header_value]="close"
+off=131 header_value complete
+off=133 headers complete method=1 v=1/1 flags=2 content_length=0
+off=133 message complete
+```
+
+## Line folding in header value with LF
+
+<!-- meta={"type": "request"} -->
+
+```http
+GET / HTTP/1.1
+Line1: abc\n\
+\tdef\n\
+ ghi\n\
+\t\tjkl\n\
+ mno \n\
+\t \tqrs\n\
+Line2: \t line2\t\n\
+Line3:\n\
+ line3\n\
+Line4: \n\
+ \n\
+Connection:\n\
+ close\n\
+\n
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=5 span[header_field]="Line1"
+off=22 header_field complete
+off=25 len=3 span[header_value]="abc"
+off=28 error code=25 reason="Missing expected CR after header value"
+```
+
+## No LF after CR
+
+<!-- meta={"type":"request"} -->
+
+```http
+GET / HTTP/1.1\rLine: 1
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=15 error code=2 reason="Expected CRLF after version"
+```
+
+## No LF after CR (lenient)
+
+<!-- meta={"type":"request-lenient-optional-lf-after-cr"} -->
+
+```http
+GET / HTTP/1.1\rLine: 1
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=15 len=4 span[header_field]="Line"
+off=20 header_field complete
+off=21 len=1 span[header_value]="1"
+```
+
+## Request starting with CRLF
+
+<!-- meta={"type": "request"} -->
+```http
+\r\nGET /url HTTP/1.1
+Header1: Value1
+
+
+```
+
+```log
+off=2 message begin
+off=2 len=3 span[method]="GET"
+off=5 method complete
+off=6 len=4 span[url]="/url"
+off=11 url complete
+off=16 len=3 span[version]="1.1"
+off=19 version complete
+off=21 len=7 span[header_field]="Header1"
+off=29 header_field complete
+off=30 len=6 span[header_value]="Value1"
+off=38 header_value complete
+off=40 headers complete method=1 v=1/1 flags=0 content_length=0
+off=40 message complete
+```
+
+## Extended Characters
+
+See nodejs/test/parallel/test-http-headers-obstext.js
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET / HTTP/1.1
+Test: DĆ¼sseldorf
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Test"
+off=21 header_field complete
+off=22 len=11 span[header_value]="DĆ¼sseldorf"
+off=35 header_value complete
+off=37 headers complete method=1 v=1/1 flags=0 content_length=0
+off=37 message complete
+```
+
+## 255 ASCII in header value
+
+Note: `Buffer.from([ 0xff ]).toString('latin1') === 'Ćæ'`.
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+OPTIONS /url HTTP/1.1
+Header1: Value1
+Header2: \xffValue2
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="OPTIONS"
+off=7 method complete
+off=8 len=4 span[url]="/url"
+off=13 url complete
+off=18 len=3 span[version]="1.1"
+off=21 version complete
+off=23 len=7 span[header_field]="Header1"
+off=31 header_field complete
+off=32 len=6 span[header_value]="Value1"
+off=40 header_value complete
+off=40 len=7 span[header_field]="Header2"
+off=48 header_field complete
+off=49 len=8 span[header_value]="ĆæValue2"
+off=59 header_value complete
+off=61 headers complete method=6 v=1/1 flags=0 content_length=0
+off=61 message complete
+```
+
+## X-SSL-Nonsense
+
+See nodejs/test/parallel/test-http-headers-obstext.js
+
+<!-- meta={"type": "request"} -->
+```http
+GET / HTTP/1.1
+X-SSL-Nonsense: -----BEGIN CERTIFICATE-----
+\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx
+\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT
+\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu
+\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV
+\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV
+\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB
+\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF
+\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR
+\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL
+\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP
+\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR
+\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG
+\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs
+\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD
+\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj
+\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj
+\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG
+\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE
+\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO
+\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1
+\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0
+\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD
+\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv
+\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3
+\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8
+\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk
+\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK
+\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu
+\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3
+\tRA==
+\t-----END CERTIFICATE-----
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=14 span[header_field]="X-SSL-Nonsense"
+off=31 header_field complete
+off=34 len=27 span[header_value]="-----BEGIN CERTIFICATE-----"
+off=63 len=65 span[header_value]="\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx"
+off=130 len=65 span[header_value]="\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT"
+off=197 len=65 span[header_value]="\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu"
+off=264 len=65 span[header_value]="\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV"
+off=331 len=65 span[header_value]="\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV"
+off=398 len=65 span[header_value]="\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB"
+off=465 len=65 span[header_value]="\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF"
+off=532 len=65 span[header_value]="\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR"
+off=599 len=65 span[header_value]="\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL"
+off=666 len=65 span[header_value]="\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP"
+off=733 len=65 span[header_value]="\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR"
+off=800 len=65 span[header_value]="\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG"
+off=867 len=66 span[header_value]="\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs"
+off=935 len=65 span[header_value]="\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD"
+off=1002 len=65 span[header_value]="\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj"
+off=1069 len=65 span[header_value]="\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj"
+off=1136 len=65 span[header_value]="\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG"
+off=1203 len=65 span[header_value]="\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE"
+off=1270 len=65 span[header_value]="\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO"
+off=1337 len=65 span[header_value]="\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1"
+off=1404 len=75 span[header_value]="\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0"
+off=1481 len=65 span[header_value]="\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD"
+off=1548 len=55 span[header_value]="\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv"
+off=1605 len=65 span[header_value]="\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3"
+off=1672 len=65 span[header_value]="\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8"
+off=1739 len=65 span[header_value]="\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk"
+off=1806 len=65 span[header_value]="\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK"
+off=1873 len=65 span[header_value]="\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu"
+off=1940 len=65 span[header_value]="\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3"
+off=2007 len=5 span[header_value]="\tRA=="
+off=2014 len=26 span[header_value]="\t-----END CERTIFICATE-----"
+off=2042 header_value complete
+off=2044 headers complete method=1 v=1/1 flags=0 content_length=0
+off=2044 message complete
+```
+
+[0]: https://github.com/nodejs/http-parser
diff --git a/llhttp/test/request/transfer-encoding.md b/llhttp/test/request/transfer-encoding.md
new file mode 100644
index 0000000..0f839bc
--- /dev/null
+++ b/llhttp/test/request/transfer-encoding.md
@@ -0,0 +1,1187 @@
+Transfer-Encoding header
+========================
+
+## `chunked`
+
+### Parsing and setting flag
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+```
+
+### Parse chunks with lowercase size
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+a
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+off=52 chunk header len=10
+off=52 len=10 span[body]="0123456789"
+off=64 chunk complete
+off=67 chunk header len=0
+off=69 chunk complete
+off=69 message complete
+```
+
+### Parse chunks with uppercase size
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+A
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+off=52 chunk header len=10
+off=52 len=10 span[body]="0123456789"
+off=64 chunk complete
+off=67 chunk header len=0
+off=69 chunk complete
+off=69 message complete
+```
+
+### POST with `Transfer-Encoding: chunked`
+
+<!-- meta={"type": "request"} -->
+```http
+POST /post_chunked_all_your_base HTTP/1.1
+Transfer-Encoding: chunked
+
+1e
+all your base are belong to us
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=27 span[url]="/post_chunked_all_your_base"
+off=33 url complete
+off=38 len=3 span[version]="1.1"
+off=41 version complete
+off=43 len=17 span[header_field]="Transfer-Encoding"
+off=61 header_field complete
+off=62 len=7 span[header_value]="chunked"
+off=71 header_value complete
+off=73 headers complete method=3 v=1/1 flags=208 content_length=0
+off=77 chunk header len=30
+off=77 len=30 span[body]="all your base are belong to us"
+off=109 chunk complete
+off=112 chunk header len=0
+off=114 chunk complete
+off=114 message complete
+```
+
+### Two chunks and triple zero prefixed end chunk
+
+<!-- meta={"type": "request"} -->
+```http
+POST /two_chunks_mult_zero_end HTTP/1.1
+Transfer-Encoding: chunked
+
+5
+hello
+6
+ world
+000
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=25 span[url]="/two_chunks_mult_zero_end"
+off=31 url complete
+off=36 len=3 span[version]="1.1"
+off=39 version complete
+off=41 len=17 span[header_field]="Transfer-Encoding"
+off=59 header_field complete
+off=60 len=7 span[header_value]="chunked"
+off=69 header_value complete
+off=71 headers complete method=3 v=1/1 flags=208 content_length=0
+off=74 chunk header len=5
+off=74 len=5 span[body]="hello"
+off=81 chunk complete
+off=84 chunk header len=6
+off=84 len=6 span[body]=" world"
+off=92 chunk complete
+off=97 chunk header len=0
+off=99 chunk complete
+off=99 message complete
+```
+
+### Trailing headers
+
+<!-- meta={"type": "request"} -->
+```http
+POST /chunked_w_trailing_headers HTTP/1.1
+Transfer-Encoding: chunked
+
+5
+hello
+6
+ world
+0
+Vary: *
+Content-Type: text/plain
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=27 span[url]="/chunked_w_trailing_headers"
+off=33 url complete
+off=38 len=3 span[version]="1.1"
+off=41 version complete
+off=43 len=17 span[header_field]="Transfer-Encoding"
+off=61 header_field complete
+off=62 len=7 span[header_value]="chunked"
+off=71 header_value complete
+off=73 headers complete method=3 v=1/1 flags=208 content_length=0
+off=76 chunk header len=5
+off=76 len=5 span[body]="hello"
+off=83 chunk complete
+off=86 chunk header len=6
+off=86 len=6 span[body]=" world"
+off=94 chunk complete
+off=97 chunk header len=0
+off=97 len=4 span[header_field]="Vary"
+off=102 header_field complete
+off=103 len=1 span[header_value]="*"
+off=106 header_value complete
+off=106 len=12 span[header_field]="Content-Type"
+off=119 header_field complete
+off=120 len=10 span[header_value]="text/plain"
+off=132 header_value complete
+off=134 chunk complete
+off=134 message complete
+```
+
+### Chunk extensions
+
+<!-- meta={"type": "request"} -->
+```http
+POST /chunked_w_unicorns_after_length HTTP/1.1
+Transfer-Encoding: chunked
+
+5;ilovew3;somuchlove=aretheseparametersfor;another=withvalue
+hello
+6;blahblah;blah
+ world
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=32 span[url]="/chunked_w_unicorns_after_length"
+off=38 url complete
+off=43 len=3 span[version]="1.1"
+off=46 version complete
+off=48 len=17 span[header_field]="Transfer-Encoding"
+off=66 header_field complete
+off=67 len=7 span[header_value]="chunked"
+off=76 header_value complete
+off=78 headers complete method=3 v=1/1 flags=208 content_length=0
+off=80 len=7 span[chunk_extension_name]="ilovew3"
+off=88 chunk_extension_name complete
+off=88 len=10 span[chunk_extension_name]="somuchlove"
+off=99 chunk_extension_name complete
+off=99 len=21 span[chunk_extension_value]="aretheseparametersfor"
+off=121 chunk_extension_value complete
+off=121 len=7 span[chunk_extension_name]="another"
+off=129 chunk_extension_name complete
+off=129 len=9 span[chunk_extension_value]="withvalue"
+off=139 chunk_extension_value complete
+off=140 chunk header len=5
+off=140 len=5 span[body]="hello"
+off=147 chunk complete
+off=149 len=8 span[chunk_extension_name]="blahblah"
+off=158 chunk_extension_name complete
+off=158 len=4 span[chunk_extension_name]="blah"
+off=163 chunk_extension_name complete
+off=164 chunk header len=6
+off=164 len=6 span[body]=" world"
+off=172 chunk complete
+off=175 chunk header len=0
+```
+
+### No semicolon before chunk extensions
+
+<!-- meta={"type": "request"} -->
+```http
+POST /chunked_w_unicorns_after_length HTTP/1.1
+Host: localhost
+Transfer-encoding: chunked
+
+2 erfrferferf
+aa
+0 rrrr
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=32 span[url]="/chunked_w_unicorns_after_length"
+off=38 url complete
+off=43 len=3 span[version]="1.1"
+off=46 version complete
+off=48 len=4 span[header_field]="Host"
+off=53 header_field complete
+off=54 len=9 span[header_value]="localhost"
+off=65 header_value complete
+off=65 len=17 span[header_field]="Transfer-encoding"
+off=83 header_field complete
+off=84 len=7 span[header_value]="chunked"
+off=93 header_value complete
+off=95 headers complete method=3 v=1/1 flags=208 content_length=0
+off=97 error code=12 reason="Invalid character in chunk size"
+```
+
+### No extension after semicolon
+
+<!-- meta={"type": "request"} -->
+```http
+POST /chunked_w_unicorns_after_length HTTP/1.1
+Host: localhost
+Transfer-encoding: chunked
+
+2;
+aa
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=32 span[url]="/chunked_w_unicorns_after_length"
+off=38 url complete
+off=43 len=3 span[version]="1.1"
+off=46 version complete
+off=48 len=4 span[header_field]="Host"
+off=53 header_field complete
+off=54 len=9 span[header_value]="localhost"
+off=65 header_value complete
+off=65 len=17 span[header_field]="Transfer-encoding"
+off=83 header_field complete
+off=84 len=7 span[header_value]="chunked"
+off=93 header_value complete
+off=95 headers complete method=3 v=1/1 flags=208 content_length=0
+off=98 error code=2 reason="Invalid character in chunk extensions"
+```
+
+
+### Chunk extensions quoting
+
+<!-- meta={"type": "request"} -->
+```http
+POST /chunked_w_unicorns_after_length HTTP/1.1
+Transfer-Encoding: chunked
+
+5;ilovew3="I \"love\"; \\extensions\\";somuchlove="aretheseparametersfor";blah;foo=bar
+hello
+6;blahblah;blah
+ world
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=32 span[url]="/chunked_w_unicorns_after_length"
+off=38 url complete
+off=43 len=3 span[version]="1.1"
+off=46 version complete
+off=48 len=17 span[header_field]="Transfer-Encoding"
+off=66 header_field complete
+off=67 len=7 span[header_value]="chunked"
+off=76 header_value complete
+off=78 headers complete method=3 v=1/1 flags=208 content_length=0
+off=80 len=7 span[chunk_extension_name]="ilovew3"
+off=88 chunk_extension_name complete
+off=88 len=28 span[chunk_extension_value]=""I \"love\"; \\extensions\\""
+off=116 chunk_extension_value complete
+off=117 len=10 span[chunk_extension_name]="somuchlove"
+off=128 chunk_extension_name complete
+off=128 len=23 span[chunk_extension_value]=""aretheseparametersfor""
+off=151 chunk_extension_value complete
+off=152 len=4 span[chunk_extension_name]="blah"
+off=157 chunk_extension_name complete
+off=157 len=3 span[chunk_extension_name]="foo"
+off=161 chunk_extension_name complete
+off=161 len=3 span[chunk_extension_value]="bar"
+off=165 chunk_extension_value complete
+off=166 chunk header len=5
+off=166 len=5 span[body]="hello"
+off=173 chunk complete
+off=175 len=8 span[chunk_extension_name]="blahblah"
+off=184 chunk_extension_name complete
+off=184 len=4 span[chunk_extension_name]="blah"
+off=189 chunk_extension_name complete
+off=190 chunk header len=6
+off=190 len=6 span[body]=" world"
+off=198 chunk complete
+off=201 chunk header len=0
+```
+
+
+### Unbalanced chunk extensions quoting
+
+<!-- meta={"type": "request"} -->
+```http
+POST /chunked_w_unicorns_after_length HTTP/1.1
+Transfer-Encoding: chunked
+
+5;ilovew3="abc";somuchlove="def; ghi
+hello
+6;blahblah;blah
+ world
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=32 span[url]="/chunked_w_unicorns_after_length"
+off=38 url complete
+off=43 len=3 span[version]="1.1"
+off=46 version complete
+off=48 len=17 span[header_field]="Transfer-Encoding"
+off=66 header_field complete
+off=67 len=7 span[header_value]="chunked"
+off=76 header_value complete
+off=78 headers complete method=3 v=1/1 flags=208 content_length=0
+off=80 len=7 span[chunk_extension_name]="ilovew3"
+off=88 chunk_extension_name complete
+off=88 len=5 span[chunk_extension_value]=""abc""
+off=93 chunk_extension_value complete
+off=94 len=10 span[chunk_extension_name]="somuchlove"
+off=105 chunk_extension_name complete
+off=105 len=9 span[chunk_extension_value]=""def; ghi"
+off=115 error code=2 reason="Invalid character in chunk extensions quoted value"
+```
+
+## Ignoring `pigeons`
+
+Requests cannot have invalid `Transfer-Encoding`. It is impossible to determine
+their body size. Not erroring would make HTTP smuggling attacks possible.
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: pigeons
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="pigeons"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=200 content_length=0
+off=49 error code=15 reason="Request has invalid `Transfer-Encoding`"
+```
+
+## POST with `Transfer-Encoding` and `Content-Length`
+
+<!-- meta={"type": "request"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: identity
+Content-Length: 5
+
+World
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=8 span[header_value]="identity"
+off=96 header_value complete
+off=96 len=14 span[header_field]="Content-Length"
+off=111 header_field complete
+off=111 error code=11 reason="Content-Length can't be present with Transfer-Encoding"
+```
+
+## POST with `Transfer-Encoding` and `Content-Length` (lenient)
+
+TODO(indutny): should we allow it even in lenient mode? (Consider disabling
+this).
+
+NOTE: `Content-Length` is ignored when `Transfer-Encoding` is present. Messages
+(in lenient mode) are read until EOF.
+
+<!-- meta={"type": "request-lenient-chunked-length"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: identity
+Content-Length: 1
+
+World
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=8 span[header_value]="identity"
+off=96 header_value complete
+off=96 len=14 span[header_field]="Content-Length"
+off=111 header_field complete
+off=112 len=1 span[header_value]="1"
+off=115 header_value complete
+off=117 headers complete method=3 v=1/1 flags=220 content_length=1
+off=117 len=5 span[body]="World"
+```
+
+## POST with empty `Transfer-Encoding` and `Content-Length` (lenient)
+
+<!-- meta={"type": "request"} -->
+```http
+POST / HTTP/1.1
+Host: foo
+Content-Length: 10
+Transfer-Encoding:
+Transfer-Encoding:
+Transfer-Encoding:
+
+2
+AA
+0
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=1 span[url]="/"
+off=7 url complete
+off=12 len=3 span[version]="1.1"
+off=15 version complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=3 span[header_value]="foo"
+off=28 header_value complete
+off=28 len=14 span[header_field]="Content-Length"
+off=43 header_field complete
+off=44 len=2 span[header_value]="10"
+off=48 header_value complete
+off=48 len=17 span[header_field]="Transfer-Encoding"
+off=66 header_field complete
+off=66 error code=15 reason="Transfer-Encoding can't be present with Content-Length"
+```
+
+## POST with `chunked` before other transfer coding names
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: chunked, deflate
+
+World
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=7 span[header_value]="chunked"
+off=94 error code=15 reason="Invalid `Transfer-Encoding` header value"
+```
+
+## POST with `chunked` and duplicate transfer-encoding
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: chunked
+Transfer-Encoding: deflate
+
+World
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=7 span[header_value]="chunked"
+off=95 header_value complete
+off=95 len=17 span[header_field]="Transfer-Encoding"
+off=113 header_field complete
+off=114 len=0 span[header_value]=""
+off=115 error code=15 reason="Invalid `Transfer-Encoding` header value"
+```
+
+## POST with `chunked` before other transfer-coding (lenient)
+
+<!-- meta={"type": "request-lenient-transfer-encoding"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: chunked, deflate
+
+World
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=16 span[header_value]="chunked, deflate"
+off=104 header_value complete
+off=106 headers complete method=3 v=1/1 flags=200 content_length=0
+off=106 len=5 span[body]="World"
+```
+
+## POST with `chunked` and duplicate transfer-encoding (lenient)
+
+<!-- meta={"type": "request-lenient-transfer-encoding"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: chunked
+Transfer-Encoding: deflate
+
+World
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=7 span[header_value]="chunked"
+off=95 header_value complete
+off=95 len=17 span[header_field]="Transfer-Encoding"
+off=113 header_field complete
+off=114 len=7 span[header_value]="deflate"
+off=123 header_value complete
+off=125 headers complete method=3 v=1/1 flags=200 content_length=0
+off=125 len=5 span[body]="World"
+```
+
+## POST with `chunked` as last transfer-encoding
+
+<!-- meta={"type": "request"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: deflate, chunked
+
+5
+World
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=16 span[header_value]="deflate, chunked"
+off=104 header_value complete
+off=106 headers complete method=3 v=1/1 flags=208 content_length=0
+off=109 chunk header len=5
+off=109 len=5 span[body]="World"
+off=116 chunk complete
+off=119 chunk header len=0
+off=121 chunk complete
+off=121 message complete
+```
+
+## POST with `chunked` as last transfer-encoding (multiple headers)
+
+<!-- meta={"type": "request"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: deflate
+Transfer-Encoding: chunked
+
+5
+World
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=7 span[header_value]="deflate"
+off=95 header_value complete
+off=95 len=17 span[header_field]="Transfer-Encoding"
+off=113 header_field complete
+off=114 len=7 span[header_value]="chunked"
+off=123 header_value complete
+off=125 headers complete method=3 v=1/1 flags=208 content_length=0
+off=128 chunk header len=5
+off=128 len=5 span[body]="World"
+off=135 chunk complete
+off=138 chunk header len=0
+off=140 chunk complete
+off=140 message complete
+```
+
+## POST with `chunkedchunked` as transfer-encoding
+
+<!-- meta={"type": "request"} -->
+```http
+POST /post_identity_body_world?q=search#hey HTTP/1.1
+Accept: */*
+Transfer-Encoding: chunkedchunked
+
+5
+World
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=4 span[method]="POST"
+off=4 method complete
+off=5 len=38 span[url]="/post_identity_body_world?q=search#hey"
+off=44 url complete
+off=49 len=3 span[version]="1.1"
+off=52 version complete
+off=54 len=6 span[header_field]="Accept"
+off=61 header_field complete
+off=62 len=3 span[header_value]="*/*"
+off=67 header_value complete
+off=67 len=17 span[header_field]="Transfer-Encoding"
+off=85 header_field complete
+off=86 len=14 span[header_value]="chunkedchunked"
+off=102 header_value complete
+off=104 headers complete method=3 v=1/1 flags=200 content_length=0
+off=104 error code=15 reason="Request has invalid `Transfer-Encoding`"
+```
+
+## Missing last-chunk
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+3
+foo
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+off=52 chunk header len=3
+off=52 len=3 span[body]="foo"
+off=57 chunk complete
+off=57 error code=12 reason="Invalid character in chunk size"
+```
+
+## Validate chunk parameters
+
+<!-- meta={"type": "request" } -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+3 \n \r\n\
+foo
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+off=51 error code=12 reason="Invalid character in chunk size"
+```
+
+## Invalid OBS fold after chunked value
+
+<!-- meta={"type": "request" } -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+ abc
+
+5
+World
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 len=5 span[header_value]=" abc"
+off=54 header_value complete
+off=56 headers complete method=4 v=1/1 flags=200 content_length=0
+off=56 error code=15 reason="Request has invalid `Transfer-Encoding`"
+```
+
+### Chunk header not terminated by CRLF
+
+<!-- meta={"type": "request" } -->
+
+```http
+GET / HTTP/1.1
+Host: a
+Connection: close
+Transfer-Encoding: chunked
+
+5\r\r;ABCD
+34
+E
+0
+
+GET / HTTP/1.1
+Host: a
+Content-Length: 5
+
+0
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=1 span[header_value]="a"
+off=25 header_value complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=6 span[header_value]="close "
+off=45 header_value complete
+off=45 len=17 span[header_field]="Transfer-Encoding"
+off=63 header_field complete
+off=64 len=8 span[header_value]="chunked "
+off=74 header_value complete
+off=76 headers complete method=1 v=1/1 flags=20a content_length=0
+off=78 error code=2 reason="Expected LF after chunk size"
+```
+
+### Chunk header not terminated by CRLF (lenient)
+
+<!-- meta={"type": "request-lenient-optional-lf-after-cr" } -->
+
+```http
+GET / HTTP/1.1
+Host: a
+Connection: close
+Transfer-Encoding: chunked
+
+6\r\r;ABCD
+33
+E
+0
+
+GET / HTTP/1.1
+Host: a
+Content-Length: 5
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=1 span[header_value]="a"
+off=25 header_value complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=6 span[header_value]="close "
+off=45 header_value complete
+off=45 len=17 span[header_field]="Transfer-Encoding"
+off=63 header_field complete
+off=64 len=8 span[header_value]="chunked "
+off=74 header_value complete
+off=76 headers complete method=1 v=1/1 flags=20a content_length=0
+off=78 chunk header len=6
+off=78 len=1 span[body]=cr
+off=79 len=5 span[body]=";ABCD"
+off=86 chunk complete
+off=90 chunk header len=51
+off=90 len=1 span[body]="E"
+off=91 len=1 span[body]=cr
+off=92 len=1 span[body]=lf
+off=93 len=1 span[body]="0"
+off=94 len=1 span[body]=cr
+off=95 len=1 span[body]=lf
+off=96 len=1 span[body]=cr
+off=97 len=1 span[body]=lf
+off=98 len=15 span[body]="GET / HTTP/1.1 "
+off=113 len=1 span[body]=cr
+off=114 len=1 span[body]=lf
+off=115 len=7 span[body]="Host: a"
+off=122 len=1 span[body]=cr
+off=123 len=1 span[body]=lf
+off=124 len=17 span[body]="Content-Length: 5"
+off=143 chunk complete
+off=146 chunk header len=0
+off=148 chunk complete
+off=148 message complete
+```
+
+### Chunk data not terminated by CRLF
+
+<!-- meta={"type": "request" } -->
+
+```http
+GET / HTTP/1.1
+Host: a
+Connection: close
+Transfer-Encoding: chunked
+
+5
+ABCDE0
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=1 span[header_value]="a"
+off=25 header_value complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=6 span[header_value]="close "
+off=45 header_value complete
+off=45 len=17 span[header_field]="Transfer-Encoding"
+off=63 header_field complete
+off=64 len=8 span[header_value]="chunked "
+off=74 header_value complete
+off=76 headers complete method=1 v=1/1 flags=20a content_length=0
+off=79 chunk header len=5
+off=79 len=5 span[body]="ABCDE"
+off=84 error code=2 reason="Expected LF after chunk data"
+```
+
+### Chunk data not terminated by CRLF (lenient)
+
+<!-- meta={"type": "request-lenient-optional-crlf-after-chunk" } -->
+
+```http
+GET / HTTP/1.1
+Host: a
+Connection: close
+Transfer-Encoding: chunked
+
+5
+ABCDE0
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=1 span[url]="/"
+off=6 url complete
+off=11 len=3 span[version]="1.1"
+off=14 version complete
+off=16 len=4 span[header_field]="Host"
+off=21 header_field complete
+off=22 len=1 span[header_value]="a"
+off=25 header_value complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=6 span[header_value]="close "
+off=45 header_value complete
+off=45 len=17 span[header_field]="Transfer-Encoding"
+off=63 header_field complete
+off=64 len=8 span[header_value]="chunked "
+off=74 header_value complete
+off=76 headers complete method=1 v=1/1 flags=20a content_length=0
+off=79 chunk header len=5
+off=79 len=5 span[body]="ABCDE"
+off=84 chunk complete
+off=87 chunk header len=0
+```
+
+## Space after chunk header
+
+<!-- meta={"type": "request"} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+a \r\n0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+off=51 error code=12 reason="Invalid character in chunk size"
+```
+
+## Space after chunk header (lenient)
+
+<!-- meta={"type": "request-lenient-spaces-after-chunk-size"} -->
+```http
+PUT /url HTTP/1.1
+Transfer-Encoding: chunked
+
+a \r\n0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="PUT"
+off=3 method complete
+off=4 len=4 span[url]="/url"
+off=9 url complete
+off=14 len=3 span[version]="1.1"
+off=17 version complete
+off=19 len=17 span[header_field]="Transfer-Encoding"
+off=37 header_field complete
+off=38 len=7 span[header_value]="chunked"
+off=47 header_value complete
+off=49 headers complete method=4 v=1/1 flags=208 content_length=0
+off=53 chunk header len=10
+off=53 len=10 span[body]="0123456789"
+off=65 chunk complete
+off=68 chunk header len=0
+off=70 chunk complete
+off=70 message complete
+```
diff --git a/llhttp/test/request/uri.md b/llhttp/test/request/uri.md
new file mode 100644
index 0000000..f7f12b0
--- /dev/null
+++ b/llhttp/test/request/uri.md
@@ -0,0 +1,243 @@
+URI
+===
+
+## Quotes in URI
+
+<!-- meta={"type": "request"} -->
+```http
+GET /with_"lovely"_quotes?foo=\"bar\" HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=33 span[url]="/with_"lovely"_quotes?foo=\"bar\""
+off=38 url complete
+off=43 len=3 span[version]="1.1"
+off=46 version complete
+off=50 headers complete method=1 v=1/1 flags=0 content_length=0
+off=50 message complete
+```
+
+## Query URL with question mark
+
+Some clients include `?` characters in query strings.
+
+<!-- meta={"type": "request"} -->
+```http
+GET /test.cgi?foo=bar?baz HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=21 span[url]="/test.cgi?foo=bar?baz"
+off=26 url complete
+off=31 len=3 span[version]="1.1"
+off=34 version complete
+off=38 headers complete method=1 v=1/1 flags=0 content_length=0
+off=38 message complete
+```
+
+## Host terminated by a query string
+
+<!-- meta={"type": "request"} -->
+```http
+GET http://hypnotoad.org?hail=all HTTP/1.1\r\n
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=29 span[url]="http://hypnotoad.org?hail=all"
+off=34 url complete
+off=39 len=3 span[version]="1.1"
+off=42 version complete
+off=46 headers complete method=1 v=1/1 flags=0 content_length=0
+off=46 message complete
+```
+
+## `host:port` terminated by a query string
+
+<!-- meta={"type": "request"} -->
+```http
+GET http://hypnotoad.org:1234?hail=all HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=34 span[url]="http://hypnotoad.org:1234?hail=all"
+off=39 url complete
+off=44 len=3 span[version]="1.1"
+off=47 version complete
+off=51 headers complete method=1 v=1/1 flags=0 content_length=0
+off=51 message complete
+```
+
+## Query URL with vertical bar character
+
+It should be allowed to have vertical bar symbol in URI: `|`.
+
+See: https://github.com/nodejs/node/issues/27584
+
+<!-- meta={"type": "request"} -->
+```http
+GET /test.cgi?query=| HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=17 span[url]="/test.cgi?query=|"
+off=22 url complete
+off=27 len=3 span[version]="1.1"
+off=30 version complete
+off=34 headers complete method=1 v=1/1 flags=0 content_length=0
+off=34 message complete
+```
+
+## `host:port` terminated by a space
+
+<!-- meta={"type": "request"} -->
+```http
+GET http://hypnotoad.org:1234 HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=25 span[url]="http://hypnotoad.org:1234"
+off=30 url complete
+off=35 len=3 span[version]="1.1"
+off=38 version complete
+off=42 headers complete method=1 v=1/1 flags=0 content_length=0
+off=42 message complete
+```
+
+## Disallow UTF-8 in URI path in strict mode
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET /Ī“Ā¶/Ī“t/pope?q=1#narf HTTP/1.1
+Host: github.com
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=5 error code=7 reason="Invalid char in url path"
+```
+
+## Fragment in URI
+
+<!-- meta={"type": "request"} -->
+```http
+GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=40 span[url]="/forums/1/topics/2375?page=1#posts-17408"
+off=45 url complete
+off=50 len=3 span[version]="1.1"
+off=53 version complete
+off=57 headers complete method=1 v=1/1 flags=0 content_length=0
+off=57 message complete
+```
+
+## Underscore in hostname
+
+<!-- meta={"type": "request"} -->
+```http
+CONNECT home_0.netscape.com:443 HTTP/1.0
+User-agent: Mozilla/1.1N
+Proxy-authorization: basic aGVsbG86d29ybGQ=
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=7 span[method]="CONNECT"
+off=7 method complete
+off=8 len=23 span[url]="home_0.netscape.com:443"
+off=32 url complete
+off=37 len=3 span[version]="1.0"
+off=40 version complete
+off=42 len=10 span[header_field]="User-agent"
+off=53 header_field complete
+off=54 len=12 span[header_value]="Mozilla/1.1N"
+off=68 header_value complete
+off=68 len=19 span[header_field]="Proxy-authorization"
+off=88 header_field complete
+off=89 len=22 span[header_value]="basic aGVsbG86d29ybGQ="
+off=113 header_value complete
+off=115 headers complete method=5 v=1/0 flags=0 content_length=0
+off=115 message complete
+off=115 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+## `host:port` and basic auth
+
+<!-- meta={"type": "request"} -->
+```http
+GET http://a%12:b!&*$@hypnotoad.org:1234/toto HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=41 span[url]="http://a%12:b!&*$@hypnotoad.org:1234/toto"
+off=46 url complete
+off=51 len=3 span[version]="1.1"
+off=54 version complete
+off=58 headers complete method=1 v=1/1 flags=0 content_length=0
+off=58 message complete
+```
+
+## Space in URI
+
+<!-- meta={"type": "request", "noScan": true} -->
+```http
+GET /foo bar/ HTTP/1.1
+
+
+```
+
+```log
+off=0 message begin
+off=0 len=3 span[method]="GET"
+off=3 method complete
+off=4 len=4 span[url]="/foo"
+off=9 url complete
+off=9 error code=8 reason="Expected HTTP/"
+```
diff --git a/llhttp/test/response/connection.md b/llhttp/test/response/connection.md
new file mode 100644
index 0000000..11f9eb6
--- /dev/null
+++ b/llhttp/test/response/connection.md
@@ -0,0 +1,647 @@
+Connection header
+=================
+
+## Proxy-Connection
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Content-Type: text/html; charset=UTF-8
+Content-Length: 11
+Proxy-Connection: close
+Date: Thu, 31 Dec 2009 20:55:48 +0000
+
+hello world
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=12 span[header_field]="Content-Type"
+off=30 header_field complete
+off=31 len=24 span[header_value]="text/html; charset=UTF-8"
+off=57 header_value complete
+off=57 len=14 span[header_field]="Content-Length"
+off=72 header_field complete
+off=73 len=2 span[header_value]="11"
+off=77 header_value complete
+off=77 len=16 span[header_field]="Proxy-Connection"
+off=94 header_field complete
+off=95 len=5 span[header_value]="close"
+off=102 header_value complete
+off=102 len=4 span[header_field]="Date"
+off=107 header_field complete
+off=108 len=31 span[header_value]="Thu, 31 Dec 2009 20:55:48 +0000"
+off=141 header_value complete
+off=143 headers complete status=200 v=1/1 flags=22 content_length=11
+off=143 len=11 span[body]="hello world"
+off=154 message complete
+```
+
+## HTTP/1.0 with keep-alive and EOF-terminated 200 status
+
+There is no `Content-Length` in this response, so even though the
+`keep-alive` is on - it should read until EOF.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.0 200 OK
+Connection: keep-alive
+
+HTTP/1.0 200 OK
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.0"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=10 span[header_field]="Connection"
+off=28 header_field complete
+off=29 len=10 span[header_value]="keep-alive"
+off=41 header_value complete
+off=43 headers complete status=200 v=1/0 flags=1 content_length=0
+off=43 len=15 span[body]="HTTP/1.0 200 OK"
+```
+
+## HTTP/1.0 with keep-alive and 204 status
+
+Responses with `204` status cannot have a body.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.0 204 No content
+Connection: keep-alive
+
+HTTP/1.0 200 OK
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.0"
+off=8 version complete
+off=13 len=10 span[status]="No content"
+off=25 status complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=10 span[header_value]="keep-alive"
+off=49 header_value complete
+off=51 headers complete status=204 v=1/0 flags=1 content_length=0
+off=51 message complete
+off=51 reset
+off=51 message begin
+off=56 len=3 span[version]="1.0"
+off=59 version complete
+off=64 len=2 span[status]="OK"
+```
+
+## HTTP/1.1 with EOF-terminated 200 status
+
+There is no `Content-Length` in this response, so even though the
+`keep-alive` is on (implicitly in HTTP 1.1) - it should read until EOF.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+
+HTTP/1.1 200 OK
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=19 headers complete status=200 v=1/1 flags=0 content_length=0
+off=19 len=15 span[body]="HTTP/1.1 200 OK"
+```
+
+## HTTP/1.1 with 204 status
+
+Responses with `204` status cannot have a body.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 204 No content
+
+HTTP/1.1 200 OK
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=10 span[status]="No content"
+off=25 status complete
+off=27 headers complete status=204 v=1/1 flags=0 content_length=0
+off=27 message complete
+off=27 reset
+off=27 message begin
+off=32 len=3 span[version]="1.1"
+off=35 version complete
+off=40 len=2 span[status]="OK"
+```
+
+## HTTP/1.1 with keep-alive disabled and 204 status
+
+<!-- meta={"type": "response" } -->
+```http
+HTTP/1.1 204 No content
+Connection: close
+
+HTTP/1.1 200 OK
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=10 span[status]="No content"
+off=25 status complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=5 span[header_value]="close"
+off=44 header_value complete
+off=46 headers complete status=204 v=1/1 flags=2 content_length=0
+off=46 message complete
+off=47 error code=5 reason="Data after `Connection: close`"
+```
+
+## HTTP/1.1 with keep-alive disabled, content-length (lenient)
+
+Parser should discard extra request in lenient mode.
+
+<!-- meta={"type": "response-lenient-data-after-close" } -->
+```http
+HTTP/1.1 200 No content
+Content-Length: 5
+Connection: close
+
+2ad731e3-4dcd-4f70-b871-0ad284b29ffc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=10 span[status]="No content"
+off=25 status complete
+off=25 len=14 span[header_field]="Content-Length"
+off=40 header_field complete
+off=41 len=1 span[header_value]="5"
+off=44 header_value complete
+off=44 len=10 span[header_field]="Connection"
+off=55 header_field complete
+off=56 len=5 span[header_value]="close"
+off=63 header_value complete
+off=65 headers complete status=200 v=1/1 flags=22 content_length=5
+off=65 len=5 span[body]="2ad73"
+off=70 message complete
+```
+
+## HTTP/1.1 with keep-alive disabled, content-length
+
+Parser should discard extra request in strict mode.
+
+<!-- meta={"type": "response" } -->
+```http
+HTTP/1.1 200 No content
+Content-Length: 5
+Connection: close
+
+2ad731e3-4dcd-4f70-b871-0ad284b29ffc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=10 span[status]="No content"
+off=25 status complete
+off=25 len=14 span[header_field]="Content-Length"
+off=40 header_field complete
+off=41 len=1 span[header_value]="5"
+off=44 header_value complete
+off=44 len=10 span[header_field]="Connection"
+off=55 header_field complete
+off=56 len=5 span[header_value]="close"
+off=63 header_value complete
+off=65 headers complete status=200 v=1/1 flags=22 content_length=5
+off=65 len=5 span[body]="2ad73"
+off=70 message complete
+off=71 error code=5 reason="Data after `Connection: close`"
+```
+
+## HTTP/1.1 with keep-alive disabled and 204 status (lenient)
+
+<!-- meta={"type": "response-lenient-keep-alive"} -->
+```http
+HTTP/1.1 204 No content
+Connection: close
+
+HTTP/1.1 200 OK
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=10 span[status]="No content"
+off=25 status complete
+off=25 len=10 span[header_field]="Connection"
+off=36 header_field complete
+off=37 len=5 span[header_value]="close"
+off=44 header_value complete
+off=46 headers complete status=204 v=1/1 flags=2 content_length=0
+off=46 message complete
+off=46 reset
+off=46 message begin
+off=51 len=3 span[version]="1.1"
+off=54 version complete
+off=59 len=2 span[status]="OK"
+```
+
+## HTTP 101 response with Upgrade and Content-Length header
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 101 Switching Protocols
+Connection: upgrade
+Upgrade: h2c
+Content-Length: 4
+
+body\
+proto
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=19 span[status]="Switching Protocols"
+off=34 status complete
+off=34 len=10 span[header_field]="Connection"
+off=45 header_field complete
+off=46 len=7 span[header_value]="upgrade"
+off=55 header_value complete
+off=55 len=7 span[header_field]="Upgrade"
+off=63 header_field complete
+off=64 len=3 span[header_value]="h2c"
+off=69 header_value complete
+off=69 len=14 span[header_field]="Content-Length"
+off=84 header_field complete
+off=85 len=1 span[header_value]="4"
+off=88 header_value complete
+off=90 headers complete status=101 v=1/1 flags=34 content_length=4
+off=90 message complete
+off=90 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+## HTTP 101 response with Upgrade and Transfer-Encoding header
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 101 Switching Protocols
+Connection: upgrade
+Upgrade: h2c
+Transfer-Encoding: chunked
+
+2
+bo
+2
+dy
+0
+
+proto
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=19 span[status]="Switching Protocols"
+off=34 status complete
+off=34 len=10 span[header_field]="Connection"
+off=45 header_field complete
+off=46 len=7 span[header_value]="upgrade"
+off=55 header_value complete
+off=55 len=7 span[header_field]="Upgrade"
+off=63 header_field complete
+off=64 len=3 span[header_value]="h2c"
+off=69 header_value complete
+off=69 len=17 span[header_field]="Transfer-Encoding"
+off=87 header_field complete
+off=88 len=7 span[header_value]="chunked"
+off=97 header_value complete
+off=99 headers complete status=101 v=1/1 flags=21c content_length=0
+off=99 message complete
+off=99 error code=22 reason="Pause on CONNECT/Upgrade"
+```
+
+## HTTP 200 response with Upgrade header
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Connection: upgrade
+Upgrade: h2c
+
+body
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=10 span[header_field]="Connection"
+off=28 header_field complete
+off=29 len=7 span[header_value]="upgrade"
+off=38 header_value complete
+off=38 len=7 span[header_field]="Upgrade"
+off=46 header_field complete
+off=47 len=3 span[header_value]="h2c"
+off=52 header_value complete
+off=54 headers complete status=200 v=1/1 flags=14 content_length=0
+off=54 len=4 span[body]="body"
+```
+
+## HTTP 200 response with Upgrade header and Content-Length
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Connection: upgrade
+Upgrade: h2c
+Content-Length: 4
+
+body
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=10 span[header_field]="Connection"
+off=28 header_field complete
+off=29 len=7 span[header_value]="upgrade"
+off=38 header_value complete
+off=38 len=7 span[header_field]="Upgrade"
+off=46 header_field complete
+off=47 len=3 span[header_value]="h2c"
+off=52 header_value complete
+off=52 len=14 span[header_field]="Content-Length"
+off=67 header_field complete
+off=68 len=1 span[header_value]="4"
+off=71 header_value complete
+off=73 headers complete status=200 v=1/1 flags=34 content_length=4
+off=73 len=4 span[body]="body"
+off=77 message complete
+```
+
+## HTTP 200 response with Upgrade header and Transfer-Encoding
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Connection: upgrade
+Upgrade: h2c
+Transfer-Encoding: chunked
+
+2
+bo
+2
+dy
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=10 span[header_field]="Connection"
+off=28 header_field complete
+off=29 len=7 span[header_value]="upgrade"
+off=38 header_value complete
+off=38 len=7 span[header_field]="Upgrade"
+off=46 header_field complete
+off=47 len=3 span[header_value]="h2c"
+off=52 header_value complete
+off=52 len=17 span[header_field]="Transfer-Encoding"
+off=70 header_field complete
+off=71 len=7 span[header_value]="chunked"
+off=80 header_value complete
+off=82 headers complete status=200 v=1/1 flags=21c content_length=0
+off=85 chunk header len=2
+off=85 len=2 span[body]="bo"
+off=89 chunk complete
+off=92 chunk header len=2
+off=92 len=2 span[body]="dy"
+off=96 chunk complete
+off=99 chunk header len=0
+off=101 chunk complete
+off=101 message complete
+```
+
+## HTTP 304 with Content-Length
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 304 Not Modified
+Content-Length: 10
+
+
+HTTP/1.1 200 OK
+Content-Length: 5
+
+hello
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=12 span[status]="Not Modified"
+off=27 status complete
+off=27 len=14 span[header_field]="Content-Length"
+off=42 header_field complete
+off=43 len=2 span[header_value]="10"
+off=47 header_value complete
+off=49 headers complete status=304 v=1/1 flags=20 content_length=10
+off=49 message complete
+off=51 reset
+off=51 message begin
+off=56 len=3 span[version]="1.1"
+off=59 version complete
+off=64 len=2 span[status]="OK"
+off=68 status complete
+off=68 len=14 span[header_field]="Content-Length"
+off=83 header_field complete
+off=84 len=1 span[header_value]="5"
+off=87 header_value complete
+off=89 headers complete status=200 v=1/1 flags=20 content_length=5
+off=89 len=5 span[body]="hello"
+off=94 message complete
+```
+
+## HTTP 304 with Transfer-Encoding
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 304 Not Modified
+Transfer-Encoding: chunked
+
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+5
+hello
+0
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=12 span[status]="Not Modified"
+off=27 status complete
+off=27 len=17 span[header_field]="Transfer-Encoding"
+off=45 header_field complete
+off=46 len=7 span[header_value]="chunked"
+off=55 header_value complete
+off=57 headers complete status=304 v=1/1 flags=208 content_length=0
+off=57 message complete
+off=57 reset
+off=57 message begin
+off=62 len=3 span[version]="1.1"
+off=65 version complete
+off=70 len=2 span[status]="OK"
+off=74 status complete
+off=74 len=17 span[header_field]="Transfer-Encoding"
+off=92 header_field complete
+off=93 len=7 span[header_value]="chunked"
+off=102 header_value complete
+off=104 headers complete status=200 v=1/1 flags=208 content_length=0
+off=107 chunk header len=5
+off=107 len=5 span[body]="hello"
+off=114 chunk complete
+off=117 chunk header len=0
+```
+
+## HTTP 100 first, then 400
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 100 Continue
+
+
+HTTP/1.1 404 Not Found
+Content-Type: text/plain; charset=utf-8
+Content-Length: 14
+Date: Fri, 15 Sep 2023 19:47:23 GMT
+Server: Python/3.10 aiohttp/4.0.0a2.dev0
+
+404: Not Found
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=8 span[status]="Continue"
+off=23 status complete
+off=25 headers complete status=100 v=1/1 flags=0 content_length=0
+off=25 message complete
+off=27 reset
+off=27 message begin
+off=32 len=3 span[version]="1.1"
+off=35 version complete
+off=40 len=9 span[status]="Not Found"
+off=51 status complete
+off=51 len=12 span[header_field]="Content-Type"
+off=64 header_field complete
+off=65 len=25 span[header_value]="text/plain; charset=utf-8"
+off=92 header_value complete
+off=92 len=14 span[header_field]="Content-Length"
+off=107 header_field complete
+off=108 len=2 span[header_value]="14"
+off=112 header_value complete
+off=112 len=4 span[header_field]="Date"
+off=117 header_field complete
+off=118 len=29 span[header_value]="Fri, 15 Sep 2023 19:47:23 GMT"
+off=149 header_value complete
+off=149 len=6 span[header_field]="Server"
+off=156 header_field complete
+off=157 len=32 span[header_value]="Python/3.10 aiohttp/4.0.0a2.dev0"
+off=191 header_value complete
+off=193 headers complete status=404 v=1/1 flags=20 content_length=14
+off=193 len=14 span[body]="404: Not Found"
+off=207 message complete
+```
+
+## HTTP 103 first, then 200
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 103 Early Hints
+Link: </styles.css>; rel=preload; as=style
+
+HTTP/1.1 200 OK
+Date: Wed, 13 Sep 2023 11:09:41 GMT
+Connection: keep-alive
+Keep-Alive: timeout=5
+Content-Length: 17
+
+response content
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=11 span[status]="Early Hints"
+off=26 status complete
+off=26 len=4 span[header_field]="Link"
+off=31 header_field complete
+off=32 len=36 span[header_value]="</styles.css>; rel=preload; as=style"
+off=70 header_value complete
+off=72 headers complete status=103 v=1/1 flags=0 content_length=0
+off=72 message complete
+off=72 reset
+off=72 message begin
+off=77 len=3 span[version]="1.1"
+off=80 version complete
+off=85 len=2 span[status]="OK"
+off=89 status complete
+off=89 len=4 span[header_field]="Date"
+off=94 header_field complete
+off=95 len=29 span[header_value]="Wed, 13 Sep 2023 11:09:41 GMT"
+off=126 header_value complete
+off=126 len=10 span[header_field]="Connection"
+off=137 header_field complete
+off=138 len=10 span[header_value]="keep-alive"
+off=150 header_value complete
+off=150 len=10 span[header_field]="Keep-Alive"
+off=161 header_field complete
+off=162 len=9 span[header_value]="timeout=5"
+off=173 header_value complete
+off=173 len=14 span[header_field]="Content-Length"
+off=188 header_field complete
+off=189 len=2 span[header_value]="17"
+off=193 header_value complete
+off=195 headers complete status=200 v=1/1 flags=21 content_length=17
+off=195 len=16 span[body]="response content"
+``` \ No newline at end of file
diff --git a/llhttp/test/response/content-length.md b/llhttp/test/response/content-length.md
new file mode 100644
index 0000000..6c33924
--- /dev/null
+++ b/llhttp/test/response/content-length.md
@@ -0,0 +1,158 @@
+Content-Length header
+=====================
+
+## Response without `Content-Length`, but with body
+
+The client should wait for the server's EOF. That is, when
+`Content-Length` is not specified, and `Connection: close`, the end of body is
+specified by the EOF.
+
+_(Compare with APACHEBENCH_GET)_
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+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
+
+<?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>
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Date"
+off=22 header_field complete
+off=23 len=29 span[header_value]="Tue, 04 Aug 2009 07:59:32 GMT"
+off=54 header_value complete
+off=54 len=6 span[header_field]="Server"
+off=61 header_field complete
+off=62 len=6 span[header_value]="Apache"
+off=70 header_value complete
+off=70 len=12 span[header_field]="X-Powered-By"
+off=83 header_field complete
+off=84 len=19 span[header_value]="Servlet/2.5 JSP/2.1"
+off=105 header_value complete
+off=105 len=12 span[header_field]="Content-Type"
+off=118 header_field complete
+off=119 len=23 span[header_value]="text/xml; charset=utf-8"
+off=144 header_value complete
+off=144 len=10 span[header_field]="Connection"
+off=155 header_field complete
+off=156 len=5 span[header_value]="close"
+off=163 header_value complete
+off=165 headers complete status=200 v=1/1 flags=2 content_length=0
+off=165 len=42 span[body]="<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+off=207 len=1 span[body]=lf
+off=208 len=80 span[body]="<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">"
+off=288 len=1 span[body]=lf
+off=289 len=17 span[body]=" <SOAP-ENV:Body>"
+off=306 len=1 span[body]=lf
+off=307 len=20 span[body]=" <SOAP-ENV:Fault>"
+off=327 len=1 span[body]=lf
+off=328 len=45 span[body]=" <faultcode>SOAP-ENV:Client</faultcode>"
+off=373 len=1 span[body]=lf
+off=374 len=46 span[body]=" <faultstring>Client Error</faultstring>"
+off=420 len=1 span[body]=lf
+off=421 len=21 span[body]=" </SOAP-ENV:Fault>"
+off=442 len=1 span[body]=lf
+off=443 len=18 span[body]=" </SOAP-ENV:Body>"
+off=461 len=1 span[body]=lf
+off=462 len=20 span[body]="</SOAP-ENV:Envelope>"
+```
+
+## Content-Length-X
+
+The header that starts with `Content-Length*` should not be treated as
+`Content-Length`.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length-X: 0
+Transfer-Encoding: chunked
+
+2
+OK
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=16 span[header_field]="Content-Length-X"
+off=34 header_field complete
+off=35 len=1 span[header_value]="0"
+off=38 header_value complete
+off=38 len=17 span[header_field]="Transfer-Encoding"
+off=56 header_field complete
+off=57 len=7 span[header_value]="chunked"
+off=66 header_value complete
+off=68 headers complete status=200 v=1/1 flags=208 content_length=0
+off=71 chunk header len=2
+off=71 len=2 span[body]="OK"
+off=75 chunk complete
+off=78 chunk header len=0
+off=80 chunk complete
+off=80 message complete
+```
+
+## Content-Length reset when no body is received
+
+<!-- meta={"type": "response", "skipBody": true} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 123
+
+HTTP/1.1 200 OK
+Content-Length: 456
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=3 span[header_value]="123"
+off=38 header_value complete
+off=40 headers complete status=200 v=1/1 flags=20 content_length=123
+off=40 skip body
+off=40 message complete
+off=40 reset
+off=40 message begin
+off=45 len=3 span[version]="1.1"
+off=48 version complete
+off=53 len=2 span[status]="OK"
+off=57 status complete
+off=57 len=14 span[header_field]="Content-Length"
+off=72 header_field complete
+off=73 len=3 span[header_value]="456"
+off=78 header_value complete
+off=80 headers complete status=200 v=1/1 flags=20 content_length=456
+off=80 skip body
+off=80 message complete
+```
diff --git a/llhttp/test/response/finish.md b/llhttp/test/response/finish.md
new file mode 100644
index 0000000..2938b83
--- /dev/null
+++ b/llhttp/test/response/finish.md
@@ -0,0 +1,23 @@
+Finish
+======
+
+Those tests check the return codes and the behavior of `llhttp_finish()` C API.
+
+## It should be safe to finish with cb after empty response
+
+<!-- meta={"type": "response-finish"} -->
+```http
+HTTP/1.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=19 headers complete status=200 v=1/1 flags=0 content_length=0
+off=NULL finish=1
+```
diff --git a/llhttp/test/response/invalid.md b/llhttp/test/response/invalid.md
new file mode 100644
index 0000000..034fc4d
--- /dev/null
+++ b/llhttp/test/response/invalid.md
@@ -0,0 +1,285 @@
+Invalid responses
+=================
+
+### Incomplete HTTP protocol
+
+<!-- meta={"type": "response"} -->
+```http
+HTP/1.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=2 error code=8 reason="Expected HTTP/"
+```
+
+### Extra digit in HTTP major version
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/01.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=1 span[version]="0"
+off=6 error code=9 reason="Expected dot"
+```
+
+### Extra digit in HTTP major version #2
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/11.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=1 span[version]="1"
+off=6 error code=9 reason="Expected dot"
+```
+
+### Extra digit in HTTP minor version
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.01 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.0"
+off=8 version complete
+off=8 error code=9 reason="Expected space after version"
+```
+-->
+
+### Tab after HTTP version
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1\t200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=8 error code=9 reason="Expected space after version"
+```
+
+### CR before response and tab after HTTP version
+
+<!-- meta={"type": "response"} -->
+```http
+\rHTTP/1.1\t200 OK
+
+
+```
+
+```log
+off=1 message begin
+off=6 len=3 span[version]="1.1"
+off=9 version complete
+off=9 error code=9 reason="Expected space after version"
+```
+
+### Headers separated by CR
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Foo: 1\rBar: 2
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=3 span[header_field]="Foo"
+off=21 header_field complete
+off=22 len=1 span[header_value]="1"
+off=24 error code=3 reason="Missing expected LF after header value"
+```
+
+### Invalid HTTP version
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/5.6 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="5.6"
+off=8 error code=9 reason="Invalid HTTP version"
+```
+
+## Invalid space after start line
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+ Host: foo
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=18 error code=30 reason="Unexpected space after start line"
+```
+
+### Extra space between HTTP version and status code
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=9 error code=13 reason="Invalid status code"
+```
+
+### Extra space between status code and reason
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=3 span[status]=" OK"
+off=18 status complete
+off=20 headers complete status=200 v=1/1 flags=0 content_length=0
+```
+
+### One-digit status code
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 2 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=10 error code=13 reason="Invalid status code"
+```
+
+### Only LFs present and no body
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK\nContent-Length: 0\n\n
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=16 error code=25 reason="Missing expected CR after response line"
+```
+
+### Only LFs present and no body (lenient)
+
+<!-- meta={"type": "response-lenient-all"} -->
+```http
+HTTP/1.1 200 OK\nContent-Length: 0\n\n
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=16 status complete
+off=16 len=14 span[header_field]="Content-Length"
+off=31 header_field complete
+off=32 len=1 span[header_value]="0"
+off=34 header_value complete
+off=35 headers complete status=200 v=1/1 flags=20 content_length=0
+off=35 message complete
+```
+
+### Only LFs present
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK\n\
+Foo: abc\n\
+Bar: def\n\
+\n\
+BODY\n\
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=16 error code=25 reason="Missing expected CR after response line"
+```
+
+### Only LFs present (lenient)
+
+<!-- meta={"type": "response-lenient-all"} -->
+```http
+HTTP/1.1 200 OK\n\
+Foo: abc\n\
+Bar: def\n\
+\n\
+BODY\n\
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=16 status complete
+off=16 len=3 span[header_field]="Foo"
+off=20 header_field complete
+off=21 len=3 span[header_value]="abc"
+off=25 header_value complete
+off=25 len=3 span[header_field]="Bar"
+off=29 header_field complete
+off=30 len=3 span[header_value]="def"
+off=34 header_value complete
+off=35 headers complete status=200 v=1/1 flags=0 content_length=0
+off=35 len=4 span[body]="BODY"
+off=39 len=1 span[body]=lf
+off=40 len=1 span[body]="\"
+``` \ No newline at end of file
diff --git a/llhttp/test/response/lenient-version.md b/llhttp/test/response/lenient-version.md
new file mode 100644
index 0000000..86c6ede
--- /dev/null
+++ b/llhttp/test/response/lenient-version.md
@@ -0,0 +1,20 @@
+Lenient HTTP version parsing
+============================
+
+### Invalid HTTP version (lenient)
+
+<!-- meta={"type": "response-lenient-version"} -->
+```http
+HTTP/5.6 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="5.6"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=19 headers complete status=200 v=5/6 flags=0 content_length=0
+```
diff --git a/llhttp/test/response/pausing.md b/llhttp/test/response/pausing.md
new file mode 100644
index 0000000..d2e870b
--- /dev/null
+++ b/llhttp/test/response/pausing.md
@@ -0,0 +1,330 @@
+Pausing
+=======
+
+### on_message_begin
+
+<!-- meta={"type": "response", "pause": "on_message_begin"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=0 pause
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_message_complete
+
+<!-- meta={"type": "response", "pause": "on_message_complete"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+off=41 pause
+```
+
+### on_version_complete
+
+<!-- meta={"type": "response", "pause": "on_version_complete"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=8 pause
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_status_complete
+
+<!-- meta={"type": "response", "pause": "on_status_complete"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 pause
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_header_field_complete
+
+<!-- meta={"type": "response", "pause": "on_header_field_complete"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=32 pause
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_header_value_complete
+
+<!-- meta={"type": "response", "pause": "on_header_value_complete"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=36 pause
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_headers_complete
+
+<!-- meta={"type": "response", "pause": "on_headers_complete"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+abc
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 pause
+off=38 len=3 span[body]="abc"
+off=41 message complete
+```
+
+### on_chunk_header
+
+<!-- meta={"type": "response", "pause": "on_chunk_header"} -->
+```http
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+a
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=17 span[header_field]="Transfer-Encoding"
+off=35 header_field complete
+off=36 len=7 span[header_value]="chunked"
+off=45 header_value complete
+off=47 headers complete status=200 v=1/1 flags=208 content_length=0
+off=50 chunk header len=10
+off=50 pause
+off=50 len=10 span[body]="0123456789"
+off=62 chunk complete
+off=65 chunk header len=0
+off=65 pause
+off=67 chunk complete
+off=67 message complete
+```
+
+### on_chunk_extension_name
+
+<!-- meta={"type": "response", "pause": "on_chunk_extension_name"} -->
+```http
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+a;foo=bar
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=17 span[header_field]="Transfer-Encoding"
+off=35 header_field complete
+off=36 len=7 span[header_value]="chunked"
+off=45 header_value complete
+off=47 headers complete status=200 v=1/1 flags=208 content_length=0
+off=49 len=3 span[chunk_extension_name]="foo"
+off=53 chunk_extension_name complete
+off=53 pause
+off=53 len=3 span[chunk_extension_value]="bar"
+off=57 chunk_extension_value complete
+off=58 chunk header len=10
+off=58 len=10 span[body]="0123456789"
+off=70 chunk complete
+off=73 chunk header len=0
+off=75 chunk complete
+off=75 message complete
+```
+
+### on_chunk_extension_value
+
+<!-- meta={"type": "response", "pause": "on_chunk_extension_value"} -->
+```http
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+a;foo=bar
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=17 span[header_field]="Transfer-Encoding"
+off=35 header_field complete
+off=36 len=7 span[header_value]="chunked"
+off=45 header_value complete
+off=47 headers complete status=200 v=1/1 flags=208 content_length=0
+off=49 len=3 span[chunk_extension_name]="foo"
+off=53 chunk_extension_name complete
+off=53 len=3 span[chunk_extension_value]="bar"
+off=57 chunk_extension_value complete
+off=57 pause
+off=58 chunk header len=10
+off=58 len=10 span[body]="0123456789"
+off=70 chunk complete
+off=73 chunk header len=0
+off=75 chunk complete
+off=75 message complete
+```
+
+### on_chunk_complete
+
+<!-- meta={"type": "response", "pause": "on_chunk_complete"} -->
+```http
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+
+a
+0123456789
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=17 span[header_field]="Transfer-Encoding"
+off=35 header_field complete
+off=36 len=7 span[header_value]="chunked"
+off=45 header_value complete
+off=47 headers complete status=200 v=1/1 flags=208 content_length=0
+off=50 chunk header len=10
+off=50 len=10 span[body]="0123456789"
+off=62 chunk complete
+off=62 pause
+off=65 chunk header len=0
+off=67 chunk complete
+off=67 pause
+off=67 message complete
+```
diff --git a/llhttp/test/response/pipelining.md b/llhttp/test/response/pipelining.md
new file mode 100644
index 0000000..01e007a
--- /dev/null
+++ b/llhttp/test/response/pipelining.md
@@ -0,0 +1,60 @@
+Pipelining
+==========
+
+## Should parse multiple events
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Content-Length: 3
+
+AAA
+HTTP/1.1 201 Created
+Content-Length: 4
+
+BBBB
+HTTP/1.1 202 Accepted
+Content-Length: 5
+
+CCCC
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=14 span[header_field]="Content-Length"
+off=32 header_field complete
+off=33 len=1 span[header_value]="3"
+off=36 header_value complete
+off=38 headers complete status=200 v=1/1 flags=20 content_length=3
+off=38 len=3 span[body]="AAA"
+off=41 message complete
+off=43 reset
+off=43 message begin
+off=48 len=3 span[version]="1.1"
+off=51 version complete
+off=56 len=7 span[status]="Created"
+off=65 status complete
+off=65 len=14 span[header_field]="Content-Length"
+off=80 header_field complete
+off=81 len=1 span[header_value]="4"
+off=84 header_value complete
+off=86 headers complete status=201 v=1/1 flags=20 content_length=4
+off=86 len=4 span[body]="BBBB"
+off=90 message complete
+off=92 reset
+off=92 message begin
+off=97 len=3 span[version]="1.1"
+off=100 version complete
+off=105 len=8 span[status]="Accepted"
+off=115 status complete
+off=115 len=14 span[header_field]="Content-Length"
+off=130 header_field complete
+off=131 len=1 span[header_value]="5"
+off=134 header_value complete
+off=136 headers complete status=202 v=1/1 flags=20 content_length=5
+off=136 len=4 span[body]="CCCC"
+``` \ No newline at end of file
diff --git a/llhttp/test/response/sample.md b/llhttp/test/response/sample.md
new file mode 100644
index 0000000..be2e82d
--- /dev/null
+++ b/llhttp/test/response/sample.md
@@ -0,0 +1,653 @@
+Sample responses
+================
+
+## Simple response
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Header1: Value1
+Header2:\t Value2
+Content-Length: 0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=7 span[header_field]="Header1"
+off=25 header_field complete
+off=26 len=6 span[header_value]="Value1"
+off=34 header_value complete
+off=34 len=7 span[header_field]="Header2"
+off=42 header_field complete
+off=44 len=6 span[header_value]="Value2"
+off=52 header_value complete
+off=52 len=14 span[header_field]="Content-Length"
+off=67 header_field complete
+off=68 len=1 span[header_value]="0"
+off=71 header_value complete
+off=73 headers complete status=200 v=1/1 flags=20 content_length=0
+off=73 message complete
+```
+
+## Error on invalid response start
+
+Every response must start with `HTTP/`.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTPER/1.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=4 error code=8 reason="Expected HTTP/"
+```
+
+## Empty body should not trigger spurious span callbacks
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=19 headers complete status=200 v=1/1 flags=0 content_length=0
+```
+
+## Google 301
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 301 Moved Permanently
+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
+
+<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>\n\
+The document has moved\n\
+<A HREF="http://www.google.com/">here</A>.
+</BODY></HTML>
+```
+_(Note the `$` char in header field)_
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=17 span[status]="Moved Permanently"
+off=32 status complete
+off=32 len=8 span[header_field]="Location"
+off=41 header_field complete
+off=42 len=22 span[header_value]="http://www.google.com/"
+off=66 header_value complete
+off=66 len=12 span[header_field]="Content-Type"
+off=79 header_field complete
+off=80 len=24 span[header_value]="text/html; charset=UTF-8"
+off=106 header_value complete
+off=106 len=4 span[header_field]="Date"
+off=111 header_field complete
+off=112 len=29 span[header_value]="Sun, 26 Apr 2009 11:11:49 GMT"
+off=143 header_value complete
+off=143 len=7 span[header_field]="Expires"
+off=151 header_field complete
+off=152 len=29 span[header_value]="Tue, 26 May 2009 11:11:49 GMT"
+off=183 header_value complete
+off=183 len=22 span[header_field]="X-$PrototypeBI-Version"
+off=206 header_field complete
+off=207 len=7 span[header_value]="1.6.0.3"
+off=216 header_value complete
+off=216 len=13 span[header_field]="Cache-Control"
+off=230 header_field complete
+off=231 len=23 span[header_value]="public, max-age=2592000"
+off=256 header_value complete
+off=256 len=6 span[header_field]="Server"
+off=263 header_field complete
+off=264 len=3 span[header_value]="gws"
+off=269 header_value complete
+off=269 len=14 span[header_field]="Content-Length"
+off=284 header_field complete
+off=286 len=5 span[header_value]="219 "
+off=293 header_value complete
+off=295 headers complete status=301 v=1/1 flags=20 content_length=219
+off=295 len=74 span[body]="<HTML><HEAD><meta http-equiv=content-type content=text/html;charset=utf-8>"
+off=369 len=1 span[body]=lf
+off=370 len=37 span[body]="<TITLE>301 Moved</TITLE></HEAD><BODY>"
+off=407 len=1 span[body]=lf
+off=408 len=18 span[body]="<H1>301 Moved</H1>"
+off=426 len=1 span[body]=lf
+off=427 len=22 span[body]="The document has moved"
+off=449 len=1 span[body]=lf
+off=450 len=42 span[body]="<A HREF="http://www.google.com/">here</A>."
+off=492 len=1 span[body]=cr
+off=493 len=1 span[body]=lf
+off=494 len=14 span[body]="</BODY></HTML>"
+```
+
+## amazon.com
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 301 MovedPermanently
+Date: Wed, 15 May 2013 17:06:33 GMT
+Server: Server
+x-amz-id-1: 0GPHKXSJQ826RK7GZEB2
+p3p: policyref="http://www.amazon.com/w3c/p3p.xml",CP="CAO DSP LAW CUR ADM IVAo IVDo CONo OTPo OUR DELi PUBi OTRi BUS PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA HEA PRE LOC GOV OTC "
+x-amz-id-2: STN69VZxIFSz9YJLbz1GDbxpbjG6Qjmmq5E3DxRhOUw+Et0p4hr7c/Q8qNcx4oAD
+Location: http://www.amazon.com/Dan-Brown/e/B000AP9DSU/ref=s9_pop_gw_al1?_encoding=UTF8&refinementId=618073011&pf_rd_m=ATVPDKIKX0DER&pf_rd_s=center-2&pf_rd_r=0SHYY5BZXN3KR20BNFAY&pf_rd_t=101&pf_rd_p=1263340922&pf_rd_i=507846
+Vary: Accept-Encoding,User-Agent
+Content-Type: text/html; charset=ISO-8859-1
+Transfer-Encoding: chunked
+
+1
+\n
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=16 span[status]="MovedPermanently"
+off=31 status complete
+off=31 len=4 span[header_field]="Date"
+off=36 header_field complete
+off=37 len=29 span[header_value]="Wed, 15 May 2013 17:06:33 GMT"
+off=68 header_value complete
+off=68 len=6 span[header_field]="Server"
+off=75 header_field complete
+off=76 len=6 span[header_value]="Server"
+off=84 header_value complete
+off=84 len=10 span[header_field]="x-amz-id-1"
+off=95 header_field complete
+off=96 len=20 span[header_value]="0GPHKXSJQ826RK7GZEB2"
+off=118 header_value complete
+off=118 len=3 span[header_field]="p3p"
+off=122 header_field complete
+off=123 len=178 span[header_value]="policyref="http://www.amazon.com/w3c/p3p.xml",CP="CAO DSP LAW CUR ADM IVAo IVDo CONo OTPo OUR DELi PUBi OTRi BUS PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA HEA PRE LOC GOV OTC ""
+off=303 header_value complete
+off=303 len=10 span[header_field]="x-amz-id-2"
+off=314 header_field complete
+off=315 len=64 span[header_value]="STN69VZxIFSz9YJLbz1GDbxpbjG6Qjmmq5E3DxRhOUw+Et0p4hr7c/Q8qNcx4oAD"
+off=381 header_value complete
+off=381 len=8 span[header_field]="Location"
+off=390 header_field complete
+off=391 len=214 span[header_value]="http://www.amazon.com/Dan-Brown/e/B000AP9DSU/ref=s9_pop_gw_al1?_encoding=UTF8&refinementId=618073011&pf_rd_m=ATVPDKIKX0DER&pf_rd_s=center-2&pf_rd_r=0SHYY5BZXN3KR20BNFAY&pf_rd_t=101&pf_rd_p=1263340922&pf_rd_i=507846"
+off=607 header_value complete
+off=607 len=4 span[header_field]="Vary"
+off=612 header_field complete
+off=613 len=26 span[header_value]="Accept-Encoding,User-Agent"
+off=641 header_value complete
+off=641 len=12 span[header_field]="Content-Type"
+off=654 header_field complete
+off=655 len=29 span[header_value]="text/html; charset=ISO-8859-1"
+off=686 header_value complete
+off=686 len=17 span[header_field]="Transfer-Encoding"
+off=704 header_field complete
+off=705 len=7 span[header_value]="chunked"
+off=714 header_value complete
+off=716 headers complete status=301 v=1/1 flags=208 content_length=0
+off=719 chunk header len=1
+off=719 len=1 span[body]=lf
+off=722 chunk complete
+off=725 chunk header len=0
+off=727 chunk complete
+off=727 message complete
+```
+
+## No headers and no body
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 404 Not Found
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=9 span[status]="Not Found"
+off=24 status complete
+off=26 headers complete status=404 v=1/1 flags=0 content_length=0
+```
+
+## No reason phrase
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 301
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=14 status complete
+off=16 headers complete status=301 v=1/1 flags=0 content_length=0
+```
+
+## Empty reason phrase after space
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 \r\n\
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=0 span[status]=""
+off=15 status complete
+off=17 headers complete status=200 v=1/1 flags=0 content_length=0
+```
+
+## No carriage ret
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK\n\
+Content-Type: text/html; charset=utf-8\n\
+Connection: close\n\
+\n\
+these headers are from http://news.ycombinator.com/
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=16 error code=25 reason="Missing expected CR after response line"
+```
+
+## No carriage ret (lenient)
+
+<!-- meta={"type": "response-lenient-optional-cr-before-lf"} -->
+```http
+HTTP/1.1 200 OK\n\
+Content-Type: text/html; charset=utf-8\n\
+Connection: close\n\
+\n\
+these headers are from http://news.ycombinator.com/
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=16 status complete
+off=16 len=12 span[header_field]="Content-Type"
+off=29 header_field complete
+off=30 len=24 span[header_value]="text/html; charset=utf-8"
+off=55 header_value complete
+off=55 len=10 span[header_field]="Connection"
+off=66 header_field complete
+off=67 len=5 span[header_value]="close"
+off=73 header_value complete
+off=74 headers complete status=200 v=1/1 flags=2 content_length=0
+off=74 len=51 span[body]="these headers are from http://news.ycombinator.com/"
+```
+
+## Underscore in header key
+
+Shown by: `curl -o /dev/null -v "http://ad.doubleclick.net/pfadx/DARTSHELLCONFIGXML;dcmt=text/xml;"`
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+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
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=6 span[header_field]="Server"
+off=24 header_field complete
+off=25 len=10 span[header_value]="DCLK-AdSvr"
+off=37 header_value complete
+off=37 len=12 span[header_field]="Content-Type"
+off=50 header_field complete
+off=51 len=8 span[header_value]="text/xml"
+off=61 header_value complete
+off=61 len=14 span[header_field]="Content-Length"
+off=76 header_field complete
+off=77 len=1 span[header_value]="0"
+off=80 header_value complete
+off=80 len=8 span[header_field]="DCLK_imp"
+off=89 header_field complete
+off=90 len=81 span[header_value]="v7;x;114750856;0-0;0;17820020;0/0;21603567/21621457/1;;~okv=;dcmt=text/xml;;~cs=o"
+off=173 header_value complete
+off=175 headers complete status=200 v=1/1 flags=20 content_length=0
+off=175 message complete
+```
+
+## bonjourmadame.fr
+
+The client should not merge two headers fields when the first one doesn't
+have a value.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.0 301 Moved Permanently
+Date: Thu, 03 Jun 2010 09:56:32 GMT
+Server: Apache/2.2.3 (Red Hat)
+Cache-Control: public
+Pragma: \r\n\
+Location: http://www.bonjourmadame.fr/
+Vary: Accept-Encoding
+Content-Length: 0
+Content-Type: text/html; charset=UTF-8
+Connection: keep-alive
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.0"
+off=8 version complete
+off=13 len=17 span[status]="Moved Permanently"
+off=32 status complete
+off=32 len=4 span[header_field]="Date"
+off=37 header_field complete
+off=38 len=29 span[header_value]="Thu, 03 Jun 2010 09:56:32 GMT"
+off=69 header_value complete
+off=69 len=6 span[header_field]="Server"
+off=76 header_field complete
+off=77 len=22 span[header_value]="Apache/2.2.3 (Red Hat)"
+off=101 header_value complete
+off=101 len=13 span[header_field]="Cache-Control"
+off=115 header_field complete
+off=116 len=6 span[header_value]="public"
+off=124 header_value complete
+off=124 len=6 span[header_field]="Pragma"
+off=131 header_field complete
+off=134 len=0 span[header_value]=""
+off=134 header_value complete
+off=134 len=8 span[header_field]="Location"
+off=143 header_field complete
+off=144 len=28 span[header_value]="http://www.bonjourmadame.fr/"
+off=174 header_value complete
+off=174 len=4 span[header_field]="Vary"
+off=179 header_field complete
+off=180 len=15 span[header_value]="Accept-Encoding"
+off=197 header_value complete
+off=197 len=14 span[header_field]="Content-Length"
+off=212 header_field complete
+off=213 len=1 span[header_value]="0"
+off=216 header_value complete
+off=216 len=12 span[header_field]="Content-Type"
+off=229 header_field complete
+off=230 len=24 span[header_value]="text/html; charset=UTF-8"
+off=256 header_value complete
+off=256 len=10 span[header_field]="Connection"
+off=267 header_field complete
+off=268 len=10 span[header_value]="keep-alive"
+off=280 header_value complete
+off=282 headers complete status=301 v=1/0 flags=21 content_length=0
+off=282 message complete
+```
+
+## Spaces in header value
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+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
+
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Date"
+off=22 header_field complete
+off=23 len=29 span[header_value]="Tue, 28 Sep 2010 01:14:13 GMT"
+off=54 header_value complete
+off=54 len=6 span[header_field]="Server"
+off=61 header_field complete
+off=62 len=6 span[header_value]="Apache"
+off=70 header_value complete
+off=70 len=13 span[header_field]="Cache-Control"
+off=84 header_field complete
+off=85 len=25 span[header_value]="no-cache, must-revalidate"
+off=112 header_value complete
+off=112 len=7 span[header_field]="Expires"
+off=120 header_field complete
+off=121 len=29 span[header_value]="Mon, 26 Jul 1997 05:00:00 GMT"
+off=152 header_value complete
+off=152 len=10 span[header_field]=".et-Cookie"
+off=163 header_field complete
+off=164 len=54 span[header_value]="PlaxoCS=1274804622353690521; path=/; domain=.plaxo.com"
+off=220 header_value complete
+off=220 len=4 span[header_field]="Vary"
+off=225 header_field complete
+off=226 len=15 span[header_value]="Accept-Encoding"
+off=243 header_value complete
+off=243 len=10 span[header_field]="_eep-Alive"
+off=254 header_field complete
+off=255 len=10 span[header_value]="timeout=45"
+off=267 header_value complete
+off=267 len=10 span[header_field]="_onnection"
+off=278 header_field complete
+off=279 len=10 span[header_value]="Keep-Alive"
+off=291 header_value complete
+off=291 len=17 span[header_field]="Transfer-Encoding"
+off=309 header_field complete
+off=310 len=7 span[header_value]="chunked"
+off=319 header_value complete
+off=319 len=12 span[header_field]="Content-Type"
+off=332 header_field complete
+off=333 len=9 span[header_value]="text/html"
+off=344 header_value complete
+off=344 len=10 span[header_field]="Connection"
+off=355 header_field complete
+off=356 len=5 span[header_value]="close"
+off=363 header_value complete
+off=365 headers complete status=200 v=1/1 flags=20a content_length=0
+off=368 chunk header len=0
+off=370 chunk complete
+off=370 message complete
+```
+
+## Spaces in header name
+
+<!-- meta={"type": "response", "noScan": true} -->
+```http
+HTTP/1.1 200 OK
+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
+
+<xml>hello</xml>
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=6 span[header_field]="Server"
+off=24 header_field complete
+off=25 len=17 span[header_value]="Microsoft-IIS/6.0"
+off=44 header_value complete
+off=44 len=12 span[header_field]="X-Powered-By"
+off=57 header_field complete
+off=58 len=7 span[header_value]="ASP.NET"
+off=67 header_value complete
+off=72 error code=10 reason="Invalid header token"
+```
+
+## Non ASCII in status line
+
+<!-- meta={"type": "response", "noScan": true} -->
+```http
+HTTP/1.1 500 Oriƫntatieprobleem
+Date: Fri, 5 Nov 2010 23:07:12 GMT+2
+Content-Length: 0
+Connection: close
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=19 span[status]="Oriƫntatieprobleem"
+off=34 status complete
+off=34 len=4 span[header_field]="Date"
+off=39 header_field complete
+off=40 len=30 span[header_value]="Fri, 5 Nov 2010 23:07:12 GMT+2"
+off=72 header_value complete
+off=72 len=14 span[header_field]="Content-Length"
+off=87 header_field complete
+off=88 len=1 span[header_value]="0"
+off=91 header_value complete
+off=91 len=10 span[header_field]="Connection"
+off=102 header_field complete
+off=103 len=5 span[header_value]="close"
+off=110 header_value complete
+off=112 headers complete status=500 v=1/1 flags=22 content_length=0
+off=112 message complete
+```
+
+## HTTP version 0.9
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/0.9 200 OK
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="0.9"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=19 headers complete status=200 v=0/9 flags=0 content_length=0
+```
+
+## No Content-Length, no Transfer-Encoding
+
+The client should wait for the server's EOF. That is, when neither
+content-length nor transfer-encoding is specified, the end of body
+is specified by the EOF.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Content-Type: text/plain
+
+hello world
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=12 span[header_field]="Content-Type"
+off=30 header_field complete
+off=31 len=10 span[header_value]="text/plain"
+off=43 header_value complete
+off=45 headers complete status=200 v=1/1 flags=0 content_length=0
+off=45 len=11 span[body]="hello world"
+```
+
+## Response starting with CRLF
+
+<!-- meta={"type": "response"} -->
+```http
+\r\nHTTP/1.1 200 OK
+Header1: Value1
+Header2:\t Value2
+Content-Length: 0
+
+
+```
+
+```log
+off=2 message begin
+off=7 len=3 span[version]="1.1"
+off=10 version complete
+off=15 len=2 span[status]="OK"
+off=19 status complete
+off=19 len=7 span[header_field]="Header1"
+off=27 header_field complete
+off=28 len=6 span[header_value]="Value1"
+off=36 header_value complete
+off=36 len=7 span[header_field]="Header2"
+off=44 header_field complete
+off=46 len=6 span[header_value]="Value2"
+off=54 header_value complete
+off=54 len=14 span[header_field]="Content-Length"
+off=69 header_field complete
+off=70 len=1 span[header_value]="0"
+off=73 header_value complete
+off=75 headers complete status=200 v=1/1 flags=20 content_length=0
+off=75 message complete
+```
diff --git a/llhttp/test/response/transfer-encoding.md b/llhttp/test/response/transfer-encoding.md
new file mode 100644
index 0000000..e1fd10a
--- /dev/null
+++ b/llhttp/test/response/transfer-encoding.md
@@ -0,0 +1,410 @@
+Transfer-Encoding header
+========================
+
+## Trailing space on chunked body
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Content-Type: text/plain
+Transfer-Encoding: chunked
+
+25 \r\n\
+This is the data in the first chunk
+
+1C
+and this is the second one
+
+0 \r\n\
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=12 span[header_field]="Content-Type"
+off=30 header_field complete
+off=31 len=10 span[header_value]="text/plain"
+off=43 header_value complete
+off=43 len=17 span[header_field]="Transfer-Encoding"
+off=61 header_field complete
+off=62 len=7 span[header_value]="chunked"
+off=71 header_value complete
+off=73 headers complete status=200 v=1/1 flags=208 content_length=0
+off=76 error code=12 reason="Invalid character in chunk size"
+```
+
+## `chunked` before other transfer-encoding
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Accept: */*
+Transfer-Encoding: chunked, deflate
+
+World
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=6 span[header_field]="Accept"
+off=24 header_field complete
+off=25 len=3 span[header_value]="*/*"
+off=30 header_value complete
+off=30 len=17 span[header_field]="Transfer-Encoding"
+off=48 header_field complete
+off=49 len=16 span[header_value]="chunked, deflate"
+off=67 header_value complete
+off=69 headers complete status=200 v=1/1 flags=200 content_length=0
+off=69 len=5 span[body]="World"
+```
+
+## multiple transfer-encoding where chunked is not the last one
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Accept: */*
+Transfer-Encoding: chunked
+Transfer-Encoding: identity
+
+World
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=6 span[header_field]="Accept"
+off=24 header_field complete
+off=25 len=3 span[header_value]="*/*"
+off=30 header_value complete
+off=30 len=17 span[header_field]="Transfer-Encoding"
+off=48 header_field complete
+off=49 len=7 span[header_value]="chunked"
+off=58 header_value complete
+off=58 len=17 span[header_field]="Transfer-Encoding"
+off=76 header_field complete
+off=77 len=8 span[header_value]="identity"
+off=87 header_value complete
+off=89 headers complete status=200 v=1/1 flags=200 content_length=0
+off=89 len=5 span[body]="World"
+```
+
+## `chunkedchunked` transfer-encoding does not enable chunked enconding
+
+This check that the word `chunked` repeat more than once (with or without spaces) does not mistakenly enables chunked encoding.
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Accept: */*
+Transfer-Encoding: chunkedchunked
+
+2
+OK
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=6 span[header_field]="Accept"
+off=24 header_field complete
+off=25 len=3 span[header_value]="*/*"
+off=30 header_value complete
+off=30 len=17 span[header_field]="Transfer-Encoding"
+off=48 header_field complete
+off=49 len=14 span[header_value]="chunkedchunked"
+off=65 header_value complete
+off=67 headers complete status=200 v=1/1 flags=200 content_length=0
+off=67 len=1 span[body]="2"
+off=68 len=1 span[body]=cr
+off=69 len=1 span[body]=lf
+off=70 len=2 span[body]="OK"
+off=72 len=1 span[body]=cr
+off=73 len=1 span[body]=lf
+off=74 len=1 span[body]="0"
+off=75 len=1 span[body]=cr
+off=76 len=1 span[body]=lf
+off=77 len=1 span[body]=cr
+off=78 len=1 span[body]=lf
+```
+
+## Chunk extensions
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Host: localhost
+Transfer-encoding: chunked
+
+5;ilovew3;somuchlove=aretheseparametersfor
+hello
+6;blahblah;blah
+ world
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=9 span[header_value]="localhost"
+off=34 header_value complete
+off=34 len=17 span[header_field]="Transfer-encoding"
+off=52 header_field complete
+off=53 len=7 span[header_value]="chunked"
+off=62 header_value complete
+off=64 headers complete status=200 v=1/1 flags=208 content_length=0
+off=66 len=7 span[chunk_extension_name]="ilovew3"
+off=74 chunk_extension_name complete
+off=74 len=10 span[chunk_extension_name]="somuchlove"
+off=85 chunk_extension_name complete
+off=85 len=21 span[chunk_extension_value]="aretheseparametersfor"
+off=107 chunk_extension_value complete
+off=108 chunk header len=5
+off=108 len=5 span[body]="hello"
+off=115 chunk complete
+off=117 len=8 span[chunk_extension_name]="blahblah"
+off=126 chunk_extension_name complete
+off=126 len=4 span[chunk_extension_name]="blah"
+off=131 chunk_extension_name complete
+off=132 chunk header len=6
+off=132 len=6 span[body]=" world"
+off=140 chunk complete
+off=143 chunk header len=0
+off=145 chunk complete
+off=145 message complete
+```
+
+## No semicolon before chunk extensions
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Host: localhost
+Transfer-encoding: chunked
+
+2 erfrferferf
+aa
+0 rrrr
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=9 span[header_value]="localhost"
+off=34 header_value complete
+off=34 len=17 span[header_field]="Transfer-encoding"
+off=52 header_field complete
+off=53 len=7 span[header_value]="chunked"
+off=62 header_value complete
+off=64 headers complete status=200 v=1/1 flags=208 content_length=0
+off=66 error code=12 reason="Invalid character in chunk size"
+```
+
+
+## No extension after semicolon
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Host: localhost
+Transfer-encoding: chunked
+
+2;
+aa
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=9 span[header_value]="localhost"
+off=34 header_value complete
+off=34 len=17 span[header_field]="Transfer-encoding"
+off=52 header_field complete
+off=53 len=7 span[header_value]="chunked"
+off=62 header_value complete
+off=64 headers complete status=200 v=1/1 flags=208 content_length=0
+off=67 error code=2 reason="Invalid character in chunk extensions"
+```
+
+
+## Chunk extensions quoting
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Host: localhost
+Transfer-Encoding: chunked
+
+5;ilovew3="I love; extensions";somuchlove="aretheseparametersfor";blah;foo=bar
+hello
+6;blahblah;blah
+ world
+0
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=9 span[header_value]="localhost"
+off=34 header_value complete
+off=34 len=17 span[header_field]="Transfer-Encoding"
+off=52 header_field complete
+off=53 len=7 span[header_value]="chunked"
+off=62 header_value complete
+off=64 headers complete status=200 v=1/1 flags=208 content_length=0
+off=66 len=7 span[chunk_extension_name]="ilovew3"
+off=74 chunk_extension_name complete
+off=74 len=20 span[chunk_extension_value]=""I love; extensions""
+off=94 chunk_extension_value complete
+off=95 len=10 span[chunk_extension_name]="somuchlove"
+off=106 chunk_extension_name complete
+off=106 len=23 span[chunk_extension_value]=""aretheseparametersfor""
+off=129 chunk_extension_value complete
+off=130 len=4 span[chunk_extension_name]="blah"
+off=135 chunk_extension_name complete
+off=135 len=3 span[chunk_extension_name]="foo"
+off=139 chunk_extension_name complete
+off=139 len=3 span[chunk_extension_value]="bar"
+off=143 chunk_extension_value complete
+off=144 chunk header len=5
+off=144 len=5 span[body]="hello"
+off=151 chunk complete
+off=153 len=8 span[chunk_extension_name]="blahblah"
+off=162 chunk_extension_name complete
+off=162 len=4 span[chunk_extension_name]="blah"
+off=167 chunk_extension_name complete
+off=168 chunk header len=6
+off=168 len=6 span[body]=" world"
+off=176 chunk complete
+off=179 chunk header len=0
+```
+
+
+## Unbalanced chunk extensions quoting
+
+<!-- meta={"type": "response"} -->
+```http
+HTTP/1.1 200 OK
+Host: localhost
+Transfer-Encoding: chunked
+
+5;ilovew3="abc";somuchlove="def; ghi
+hello
+6;blahblah;blah
+ world
+0
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=4 span[header_field]="Host"
+off=22 header_field complete
+off=23 len=9 span[header_value]="localhost"
+off=34 header_value complete
+off=34 len=17 span[header_field]="Transfer-Encoding"
+off=52 header_field complete
+off=53 len=7 span[header_value]="chunked"
+off=62 header_value complete
+off=64 headers complete status=200 v=1/1 flags=208 content_length=0
+off=66 len=7 span[chunk_extension_name]="ilovew3"
+off=74 chunk_extension_name complete
+off=74 len=5 span[chunk_extension_value]=""abc""
+off=79 chunk_extension_value complete
+off=80 len=10 span[chunk_extension_name]="somuchlove"
+off=91 chunk_extension_name complete
+off=91 len=9 span[chunk_extension_value]=""def; ghi"
+off=101 error code=2 reason="Invalid character in chunk extensions quoted value"
+```
+
+
+## Invalid OBS fold after chunked value
+
+<!-- meta={"type": "response" } -->
+```http
+HTTP/1.1 200 OK
+Transfer-Encoding: chunked
+ abc
+
+5
+World
+0
+
+
+```
+
+```log
+off=0 message begin
+off=5 len=3 span[version]="1.1"
+off=8 version complete
+off=13 len=2 span[status]="OK"
+off=17 status complete
+off=17 len=17 span[header_field]="Transfer-Encoding"
+off=35 header_field complete
+off=36 len=7 span[header_value]="chunked"
+off=45 len=5 span[header_value]=" abc"
+off=52 header_value complete
+off=54 headers complete status=200 v=1/1 flags=200 content_length=0
+off=54 len=1 span[body]="5"
+off=55 len=1 span[body]=cr
+off=56 len=1 span[body]=lf
+off=57 len=5 span[body]="World"
+off=62 len=1 span[body]=cr
+off=63 len=1 span[body]=lf
+off=64 len=1 span[body]="0"
+off=65 len=1 span[body]=cr
+off=66 len=1 span[body]=lf
+off=67 len=1 span[body]=cr
+off=68 len=1 span[body]=lf
+```
+
diff --git a/llhttp/test/url.md b/llhttp/test/url.md
new file mode 100644
index 0000000..13a1b01
--- /dev/null
+++ b/llhttp/test/url.md
@@ -0,0 +1,261 @@
+# URL tests
+
+## Absolute URL
+
+```url
+http://example.com/path?query=value#schema
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=11 span[url.host]="example.com"
+off=18 len=5 span[url.path]="/path"
+off=24 len=11 span[url.query]="query=value"
+off=36 len=6 span[url.fragment]="schema"
+```
+
+## Relative URL
+
+```url
+/path?query=value#schema
+```
+
+```log
+off=0 len=5 span[url.path]="/path"
+off=6 len=11 span[url.query]="query=value"
+off=18 len=6 span[url.fragment]="schema"
+```
+
+## Failing on broken schema
+
+<!-- meta={"noScan": true} -->
+```url
+schema:/path?query=value#schema
+```
+
+```log
+off=0 len=6 span[url.schema]="schema"
+off=8 error code=7 reason="Unexpected char in url schema"
+```
+
+## Proxy request
+
+```url
+http://hostname/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=8 span[url.host]="hostname"
+off=15 len=1 span[url.path]="/"
+```
+
+## Proxy request with port
+
+```url
+http://hostname:444/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=12 span[url.host]="hostname:444"
+off=19 len=1 span[url.path]="/"
+```
+
+## Proxy IPv6 request
+
+```url
+http://[1:2::3:4]/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=10 span[url.host]="[1:2::3:4]"
+off=17 len=1 span[url.path]="/"
+```
+
+## Proxy IPv6 request with port
+
+```url
+http://[1:2::3:4]:67/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=13 span[url.host]="[1:2::3:4]:67"
+off=20 len=1 span[url.path]="/"
+```
+
+## IPv4 in IPv6 address
+
+```url
+http://[2001:0000:0000:0000:0000:0000:1.9.1.1]/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=39 span[url.host]="[2001:0000:0000:0000:0000:0000:1.9.1.1]"
+off=46 len=1 span[url.path]="/"
+```
+
+## Extra `?` in query string
+
+```url
+http://a.tbcdn.cn/p/fp/2010c/??fp-header-min.css,fp-base-min.css,\
+fp-channel-min.css,fp-product-min.css,fp-mall-min.css,fp-category-min.css,\
+fp-sub-min.css,fp-gdp4p-min.css,fp-css3-min.css,fp-misc-min.css?t=20101022.css
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=10 span[url.host]="a.tbcdn.cn"
+off=17 len=12 span[url.path]="/p/fp/2010c/"
+off=30 len=187 span[url.query]="?fp-header-min.css,fp-base-min.css,fp-channel-min.css,fp-product-min.css,fp-mall-min.css,fp-category-min.css,fp-sub-min.css,fp-gdp4p-min.css,fp-css3-min.css,fp-misc-min.css?t=20101022.css"
+```
+
+## URL encoded space
+
+```url
+/toto.html?toto=a%20b
+```
+
+```log
+off=0 len=10 span[url.path]="/toto.html"
+off=11 len=10 span[url.query]="toto=a%20b"
+```
+
+## URL fragment
+
+```url
+/toto.html#titi
+```
+
+```log
+off=0 len=10 span[url.path]="/toto.html"
+off=11 len=4 span[url.fragment]="titi"
+```
+
+## Complex URL fragment
+
+```url
+http://www.webmasterworld.com/r.cgi?f=21&d=8405&url=\
+http://www.example.com/index.html?foo=bar&hello=world#midpage
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=22 span[url.host]="www.webmasterworld.com"
+off=29 len=6 span[url.path]="/r.cgi"
+off=36 len=69 span[url.query]="f=21&d=8405&url=http://www.example.com/index.html?foo=bar&hello=world"
+off=106 len=7 span[url.fragment]="midpage"
+```
+
+## Complex URL from node.js url parser doc
+
+```url
+http://host.com:8080/p/a/t/h?query=string#hash
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=13 span[url.host]="host.com:8080"
+off=20 len=8 span[url.path]="/p/a/t/h"
+off=29 len=12 span[url.query]="query=string"
+off=42 len=4 span[url.fragment]="hash"
+```
+
+## Complex URL with basic auth from node.js url parser doc
+
+```url
+http://a:b@host.com:8080/p/a/t/h?query=string#hash
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=17 span[url.host]="a:b@host.com:8080"
+off=24 len=8 span[url.path]="/p/a/t/h"
+off=33 len=12 span[url.query]="query=string"
+off=46 len=4 span[url.fragment]="hash"
+```
+
+## Double `@`
+
+<!-- meta={"noScan": true} -->
+```url
+http://a:b@@hostname:443/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=12 error code=7 reason="Double @ in url"
+```
+
+## Proxy basic auth with url encoded space
+
+```url
+http://a%20:b@host.com/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=15 span[url.host]="a%20:b@host.com"
+off=22 len=1 span[url.path]="/"
+```
+
+## Proxy basic auth with unreserved chars
+
+```url
+http://a!;-_!=+$@host.com/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=18 span[url.host]="a!;-_!=+$@host.com"
+off=25 len=1 span[url.path]="/"
+```
+
+## IPv6 address with Zone ID
+
+```url
+http://[fe80::a%25eth0]/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=16 span[url.host]="[fe80::a%25eth0]"
+off=23 len=1 span[url.path]="/"
+```
+
+## IPv6 address with Zone ID, but `%` is not percent-encoded
+
+```url
+http://[fe80::a%eth0]/
+```
+
+```log
+off=0 len=4 span[url.schema]="http"
+off=7 len=14 span[url.host]="[fe80::a%eth0]"
+off=21 len=1 span[url.path]="/"
+```
+
+## Disallow tab in URL
+
+<!-- meta={ "noScan": true} -->
+```url
+/foo\tbar/
+```
+
+```log
+off=5 error code=7 reason="Invalid characters in url"
+```
+
+## Disallow form-feed in URL
+
+<!-- meta={ "noScan": true} -->
+```url
+/foo\fbar/
+```
+
+```log
+off=5 error code=7 reason="Invalid characters in url"
+```