v0.19: HTTP bug fixes #1503

Merged
vyzo merged 15 commits from jay/gerbil:v0.19-http-bug-fixes into v0.19-staging 2026-09-19 06:12:53 +00:00
Member

Bug fixes for HTTP std module:

  • Validate request-line syntax, header names/values and line endings; normalize header case and whitespace. Return 400 for request-head protocol errors without reclassifying application errors.
  • Require one Host for HTTP/1.1 and reject duplicate Host fields. Validate decimal Content-Length values and agreement between duplicates. Reject Transfer-Encoding/Content-Length conflicts and unsupported transfer codings; only a single chunked coding is supported.
  • Send 100 Continue before body reads for HTTP/1.1 requests with a single case-insensitive Expect: 100-continue field and positive Content-Length or chunked framing. Invalid heads receive no acknowledgment; other Expect forms remain ignored.
  • In a separate commit, fix the client to consume interim 100 Continue responses and return the final response. Preserve WebSocket 101 handling.

Public request/response structures are unchanged.

Tests

  • Server: 29/43 before → 110/110 after, fixing all 14 existing failures and adding 67 regression cases.
  • HTTP client, chunked I/O and WebSocket: 8/8 before → 10/10 after, including two new client regressions for fixed-length and chunked final responses.
  • After: 120/120 in both source and optimized modes.

Benchmark

One sequential before/after pair: f0badc77 without this PR’s fixes versus current b9892378. Verified prebuilt executables were reused; no rebuild was performed for this rerun.

  • Ryzen 9 7940HS/Linux host
  • Vegeta 12.13.0
  • 50 requests/s
  • four workers/connections
  • keepalive disabled
  • 5 seconds of warmup, 30 seconds of measurement

Latencies are milliseconds; p95/p99 are the 95th/99th percentiles.

Workload Before median / p95 / p99 After median / p95 / p99 Requests/s before → after
Simple GET 0.677 / 1.092 / 1.865 0.635 / 1.047 / 1.341 50.031 → 50.033
Chunked GET 0.766 / 1.192 / 1.662 0.772 / 1.198 / 1.514 50.032 → 50.032
12-byte echo 0.757 / 1.171 / 1.886 0.774 / 1.160 / 1.575 50.031 → 50.032
1 MiB echo 4.019 / 4.454 / 5.461 4.070 / 4.384 / 5.055 50.027 → 50.027

All 14,000 requests, including warmups, returned HTTP 200 with no errors or timeouts. Median latency changes ranged from -6.2% to +2.3% in this pair; p99 decreased across all four workloads. Mean CPU utilization was 5.75% before and 5.70% after. This single fixed-load comparison is not a capacity test or proof of performance neutrality.

Bug fixes for HTTP std module: - Validate request-line syntax, header names/values and line endings; normalize header case and whitespace. Return `400` for request-head protocol errors without reclassifying application errors. - Require one `Host` for HTTP/1.1 and reject duplicate Host fields. Validate decimal `Content-Length` values and agreement between duplicates. Reject `Transfer-Encoding`/`Content-Length` conflicts and unsupported transfer codings; only a single `chunked` coding is supported. - Send `100 Continue` before body reads for HTTP/1.1 requests with a single case-insensitive `Expect: 100-continue` field and positive `Content-Length` or chunked framing. Invalid heads receive no acknowledgment; other Expect forms remain ignored. - In a separate commit, fix the client to consume interim `100 Continue` responses and return the final response. Preserve WebSocket `101` handling. Public request/response structures are unchanged. ## Tests - Server: **29/43 before → 110/110 after**, fixing all 14 existing failures and adding 67 regression cases. - HTTP client, chunked I/O and WebSocket: **8/8 before → 10/10 after**, including two new client regressions for fixed-length and chunked final responses. - After: **120/120 in both source and optimized modes**. ## Benchmark One sequential before/after pair: `f0badc77` without this PR’s fixes versus current `b9892378`. Verified prebuilt executables were reused; no rebuild was performed for this rerun. - Ryzen 9 7940HS/Linux host - Vegeta 12.13.0 - 50 requests/s - four workers/connections - keepalive disabled - 5 seconds of warmup, 30 seconds of measurement Latencies are milliseconds; p95/p99 are the 95th/99th percentiles. | Workload | Before median / p95 / p99 | After median / p95 / p99 | Requests/s before → after | |---|---:|---:|---:| | Simple GET | 0.677 / 1.092 / 1.865 | 0.635 / 1.047 / 1.341 | 50.031 → 50.033 | | Chunked GET | 0.766 / 1.192 / 1.662 | 0.772 / 1.198 / 1.514 | 50.032 → 50.032 | | 12-byte echo | 0.757 / 1.171 / 1.886 | 0.774 / 1.160 / 1.575 | 50.031 → 50.032 | | 1 MiB echo | 4.019 / 4.454 / 5.461 | 4.070 / 4.384 / 5.055 | 50.027 → 50.027 | All **14,000 requests**, including warmups, returned HTTP 200 with no errors or timeouts. Median latency changes ranged from -6.2% to +2.3% in this pair; p99 decreased across all four workloads. Mean CPU utilization was 5.75% before and 5.70% after. This single fixed-load comparison is not a capacity test or proof of performance neutrality.
jay added 11 commits 2026-09-18 16:18:30 +00:00
jay requested reviews from vyzo, fare, HMarcien 2026-09-18 16:37:17 +00:00
jay changed title from WIP: v0.19: HTTP bug fixes to v0.19: HTTP bug fixes 2026-09-18 16:37:31 +00:00
vyzo requested changes 2026-09-18 17:36:24 +00:00
Dismissed
vyzo left a comment

I request some changes, the code can become better.

The main request is to have a base error class for the errors that result in 400 so that we don't have to check all of them exhaustively. that's a code quality issue.

I also left some comments about potential optimizations, i think it is worth doing them to avoid some allocations and astra can handle it.

I request some changes, the code can become better. The main request is to have a base error class for the errors that result in 400 so that we don't have to check all of them exhaustively. that's a code quality issue. I also left some comments about potential optimizations, i think it is worth doing them to avoid some allocations and astra can handle it.
@ -73,0 +86,4 @@
(and ;; string-split drops an empty final field after a trailing SP.
(not (char=? (string-ref line (fx1- (string-length line))) #\space))
(fx> method-length 0)
(let loop ((i 0 :- :fixnum))
Owner

i think a better way to validate the method is to have a statically constructed hash table with method tables and just lookup there, no need to parse it at all. the methods are defined by the standard anyway.

i think a better way to validate the method is to have a statically constructed hash table with method tables and just lookup there, no need to parse it at all. the methods are defined by the standard anyway.
@ -155,0 +242,4 @@
where: read-header!)))
(validate (fx1+ i))))
(cons key
(let trim-right ((stop end :- :fixnum))
Owner

i have the feeling this could be made more efficient by fusing with the header read loop. we could avoid the intermediate string allocation and the substring.

i have the feeling this could be made more efficient by fusing with the header read loop. we could avoid the intermediate string allocation and the substring.
@ -20,0 +19,4 @@
new-response-handler
validate-request-host!
request-content-length
MalformedHost
Owner

you can struct-out export all the error classes.

you can struct-out export all the error classes.
@ -20,0 +26,4 @@
MalformedRequestFraming
MalformedRequestFraming?)
(deferror-class (MalformedHost IOError) ())
Owner

if all these result to the same error code (400) you can make a base error class and inherit from there. this also goes for the other utility error classes.

if all these result to the same error code (400) you can make a base error class and inherit from there. this also goes for the other utility error classes.
@ -281,1 +312,3 @@
(cut display-exception e <>))))
(try
(if (and reading-request-head?
(or (MalformedRequestLine? e)
Owner

here the base error class comes handy, no need for 10 or cases, just check the base class predicate.

here the base error class comes handy, no need for 10 or cases, just check the base class predicate.
@ -73,0 +107,4 @@
(char<=? #\0 (string-ref proto 5) #\9)
(char=? (string-ref proto 6) #\.)
(char<=? #\0 (string-ref proto 7) #\9))
(raise/context (MalformedRequestLine "invalid request line" irritants: [line])
Owner

you should define a raise macro with defraise/context and use it, it handles things nicely and most importantly puts the abort annotation that informs the compiler that it is a computational cut.

same for all the raises of input errors.

you should define a raise macro with defraise/context and use it, it handles things nicely and most importantly puts the abort annotation that informs the compiler that it is a computational cut. same for all the raises of input errors.
Author
Member

@vyzo addressed your feedback. apologies for not reviewing this more before opening, I was on the road earlier and just pushed what I had!

@vyzo addressed your feedback. apologies for not reviewing this more before opening, I was on the road earlier and just pushed what I had!
jay requested review from vyzo 2026-09-19 00:06:41 +00:00
Owner

hey no worries, that's what review is for. astra is a great programmer, but still needs some advice.

hey no worries, that's what review is for. astra is a great programmer, but still needs some advice.
vyzo approved these changes 2026-09-19 06:12:46 +00:00
vyzo left a comment

looks good!

looks good!
vyzo merged commit 8df68cb309 into v0.19-staging 2026-09-19 06:12:53 +00:00
vyzo referenced this pull request from a commit 2026-09-19 06:12:54 +00:00
vyzo deleted branch v0.19-http-bug-fixes 2026-09-19 06:13:30 +00:00
Sign in to join this conversation.
No description provided.