Skip to content

Commit 92cf9ea

Browse files
authored
Merge pull request #17 from carpentry-org/claude/response-pipeline-fixes
Fix three response pipeline bugs
2 parents 4c83832 + 35d3b4f commit 92cf9ea

3 files changed

Lines changed: 78 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@
2929
(§5.1). The upgrade handshake validates `Sec-WebSocket-Version: 13` and
3030
responds with 426 Upgrade Required if the version is missing or wrong (§4.2.1).
3131

32+
### Fixed
33+
34+
- **Content-Length no longer sent with chunked encoding.** `web-finalize-response`
35+
now skips the `Content-Length` header when `Transfer-Encoding` is already set,
36+
fixing an RFC 7230 §3.3.2 violation.
37+
- **`log-after` no longer crashes when `_start` is missing.** The after-hook used
38+
`Maybe.unsafe-from` on the parsed start time, which panicked if `log-before`
39+
was not registered. Now falls back to `0l`.
40+
- **File descriptor leak on `fstat` failure.** When `sendfile` opened a file but
41+
`fstat` failed, the fd was stored in `ConnState` but never closed. The fd is
42+
now closed immediately on `fstat` failure.
43+
3244
### Changed
3345

3446
- `WSFrame` gains `rsv` (`Int`) and `masked` (`Bool`) fields in addition to

test/web.carp

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,4 +461,38 @@
461461
&(Result.unsafe-from-success
462462
(Request.parse
463463
"POST / HTTP/1.1\r\nContent-Type: application/json\r\n\r\n{}"))))
464-
"decode-multipart-request empty for non-multipart"))
464+
"decode-multipart-request empty for non-multipart")
465+
466+
; ---------------------------------------------------------------------------
467+
; Response finalization tests
468+
; ---------------------------------------------------------------------------
469+
470+
; -- finalize omits Content-Length for chunked responses --
471+
(assert-false test
472+
(Map.contains?
473+
(Response.headers
474+
&(web-finalize-response
475+
(Response.chunked 200 @"text/plain" &[@"hello"])
476+
true))
477+
"Content-Length")
478+
"finalize omits Content-Length when Transfer-Encoding is set")
479+
480+
; -- finalize adds Content-Length for normal responses --
481+
(assert-true test
482+
(Map.contains?
483+
(Response.headers &(web-finalize-response (Response.text @"hi") true))
484+
"Content-Length")
485+
"finalize adds Content-Length for normal responses")
486+
487+
; ---------------------------------------------------------------------------
488+
; log-after safety tests
489+
; ---------------------------------------------------------------------------
490+
491+
; -- log-after does not crash when _start is missing --
492+
(assert-equal test
493+
200
494+
(let [req (Result.unsafe-from-success
495+
(Request.parse "GET / HTTP/1.1\r\nHost: x\r\n\r\n"))
496+
params (the (Map String String) {})]
497+
@(Response.code &(log-after &req &params (Response.text @"ok"))))
498+
"log-after does not crash without _start param"))

web.carp

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -939,14 +939,15 @@ responses.")
939939

940940
(defn web-finalize-response [resp keep-alive]
941941
(let [body-len (String.length (Response.body &resp))
942-
cl-k @"Content-Length"
943-
cl-v [(Int.str body-len)]
942+
has-te (Map.contains? (Response.headers &resp) "Transfer-Encoding")
944943
conn-k @"Connection"
945944
conn-v [(if keep-alive @"keep-alive" @"close")]
946-
h (Map.put
947-
(Map.put @(Response.headers &resp) &cl-k &cl-v)
948-
&conn-k
949-
&conn-v)]
945+
h (let [base (Map.put @(Response.headers &resp) &conn-k &conn-v)]
946+
(if has-te
947+
base
948+
(let [cl-k @"Content-Length"
949+
cl-v [(Int.str body-len)]]
950+
(Map.put base &cl-k &cl-v))))]
950951
(Response.set-headers resp h)))
951952

952953
(defn read-request-buf [client buf]
@@ -1675,17 +1676,19 @@ fallback.")
16751676
(if (< ffd 0)
16761677
-1l
16771678
(let [sz (IO.Raw.fstat-size ffd)]
1678-
(do
1679-
(Map.put! (ConnState.sf-fds cs)
1680-
&fd
1681-
&ffd)
1682-
(Map.put! (ConnState.sf-offsets cs)
1683-
&fd
1684-
&0l)
1685-
(Map.put! (ConnState.sf-sizes cs)
1686-
&fd
1687-
&sz)
1688-
sz))))
1679+
(if (< sz 0l)
1680+
(do (ignore (IO.Raw.close-fd ffd)) -1l)
1681+
(do
1682+
(Map.put! (ConnState.sf-fds cs)
1683+
&fd
1684+
&ffd)
1685+
(Map.put! (ConnState.sf-offsets cs)
1686+
&fd
1687+
&0l)
1688+
(Map.put! (ConnState.sf-sizes cs)
1689+
&fd
1690+
&sz)
1691+
sz)))))
16891692
(Maybe.Nothing) -1l)
16901693
actual-resp (if (and (Maybe.just? &sf-path)
16911694
(< sf-result 0l))
@@ -1925,16 +1928,17 @@ stdout. Reads the start time from `_start` in params, set by
19251928
(GET \"/\" hello))
19261929
```")
19271930
(defn log-after [req params resp]
1928-
(let [start (Maybe.unsafe-from (Long.from-string &(Map.get params "_start")))
1929-
now (Uint64.to-long (System.nanotime))
1930-
elapsed (/ (- now start) 1000000l)]
1931-
(do
1932-
(Log.info "%s /%s %d %ldms"
1933-
(Request.verb req)
1934-
&(web-request-path req)
1935-
(Response.code &resp)
1936-
elapsed)
1937-
resp)))
1931+
(let-do [start (match (Long.from-string &(Map.get params "_start"))
1932+
(Maybe.Just v) v
1933+
(Maybe.Nothing) 0l)
1934+
now (Uint64.to-long (System.nanotime))
1935+
elapsed (/ (- now start) 1000000l)]
1936+
(Log.info "%s /%s %d %ldms"
1937+
(Request.verb req)
1938+
&(web-request-path req)
1939+
(Response.code &resp)
1940+
elapsed)
1941+
resp))
19381942

19391943
; ---------------------------------------------------------------------------
19401944
; defserver: generate a main that builds an App and starts serving

0 commit comments

Comments
 (0)