Add configurable request timeouts via Config type#4
Conversation
Introduce a Config type with connect-timeout and read-timeout fields (in seconds, 0 = no timeout). Add -with-config variants for all public request functions: request-with-config, request-stream-with-config, get-with-config, post-with-config, put-with-config, del-with-config, and patch-with-config. Connect-timeout uses TcpStream.connect-timeout for plain HTTP. Read-timeout uses set-timeout on both TcpStream and TlsStream. Also adds Connection.set-timeout as a dispatch helper.
There was a problem hiding this comment.
Build & Tests
CI: ubuntu-latest passes, macos-latest still pending. No local Carp compiler available for independent build verification.
Findings
Checked out the branch and read the full http-client.carp, test file, and README.
Core implementation is sound. Ownership is clean — config is passed as &Config through the entire call chain, all field accessors use @(...) to copy the Int out. Timeouts are applied at the right points (after connect, before any reads). The redirect loop correctly reuses the same config reference on each hop. Documentation is thorough.
Specific findings:
1. Dead code: Connection.set-timeout is defined but never called.
connect-with-config calls TcpStream.set-timeout and TlsStream.set-timeout directly on the unwrapped streams. The Connection.set-timeout helper at lines 51-54 is unused. Either use it in connect-with-config (cleaner) or remove it.
2. ~80 lines of duplicated redirect logic.
request-stream-with-config (lines ~526-604) is a near-exact copy of request-stream-with-max-redirects (lines ~426-499). The only differences: calling build-and-send-with-config instead of build-and-send, and hardcoding default-max-redirects. If a bug is found in redirect handling, it must be fixed in two places. Consider refactoring into a single internal function that accepts both a config and max-redirects, with the public functions as thin wrappers.
3. Missing API surface: no way to combine config with custom max-redirects.
request-stream-with-config hardcodes default-max-redirects. Users who need both timeouts and a custom redirect limit have no option. Adding a request-stream-with-config-and-max-redirects (or folding max-redirects into Config) would close this gap.
4. No negative timeout validation.
connect-with-config checks > 0 before applying timeouts, so negative values silently behave like 0 (no timeout). This is arguably fine, but the doc string only says "0 means no timeout" — it should mention negative values too, or Config.init should clamp/reject them.
5. HTTPS connect-timeout is silently ignored.
When scheme is "https", connect-with-config calls TlsStream.connect without a timeout parameter. This IS documented in the doc string and README, which is good. But a user passing Config.init 5 10 for an HTTPS URL might not realize only the read-timeout is active. Not a bug, but worth a code comment near the TLS branch.
6. Missing test coverage:
- No test for connect-timeout specifically (all config tests use
connect-timeout=0). - No tests for
del-with-config,put-with-config, orpatch-with-config. - No edge case tests for negative timeouts.
- The read-timeout test depends on
httpbin.org/delay/10— fragile but consistent with existing tests.
7. Pre-existing: unused path binding in both build-and-send and build-and-send-with-config. Not introduced by this PR, just carried forward.
Verdict: revise
The core timeout implementation is correct and well-documented, but:
- macos CI is still pending — wait for it before merging.
- Dead code (
Connection.set-timeout) should be used or removed. - 80 lines of duplicated redirect logic is a real maintainability risk — refactor into a shared internal function.
- Missing
max-redirects+ config combination leaves an API gap.
Fixing the dead code and duplication are the priority items. The missing edge-case tests are nice-to-have.
hellerve
left a comment
There was a problem hiding this comment.
Apart from the comments raised by the review agent, I’d also like to point out that the type name Config is likely too generic.
- Rename Config to RequestConfig (hellerve: too generic) - Add max-redirects field to RequestConfig - Remove dead Connection.set-timeout - Refactor duplicated redirect loop into shared request-stream- internal - Remove redundant connect/build-and-send (config path handles both) - Fix unused path binding in build-and-send - Document negative timeout behavior (treated same as 0)
|
Addressed all review feedback: hellerve —
carpentry-reviewer — dead code (
carpentry-reviewer — ~80 lines duplicated redirect logic:
carpentry-reviewer — missing API surface (config + custom max-redirects):
carpentry-reviewer — negative timeout documentation:
carpentry-reviewer — unused
Net result: -101 lines, one redirect loop instead of two, one connect/build-and-send pair instead of two. Type-checks clean ( |
There was a problem hiding this comment.
Build & Tests
Build: pass (carp --check http-client.carp — only pre-existing prn-for-TlsStream warnings)
Tests: cannot run locally (no Carp compiler on ARM/Linux)
CI: pass (macOS + Ubuntu, all tests green)
Prior feedback
| Issue | Status |
|---|---|
Config name too generic (hellerve) |
Fixed ✓ — renamed to RequestConfig everywhere (type, module, docs, tests, README) |
Dead code: Connection.set-timeout (round 1) |
Fixed ✓ — removed entirely. Timeouts applied directly via TcpStream.set-timeout/TlsStream.set-timeout in Client.connect (lines 236, 247, 258) |
| ~80 lines duplicated redirect logic (round 1) | Fixed ✓ — single request-stream- internal function (lines 384-458), both request-stream-with-max-redirects and request-stream-with-config are thin wrappers |
| Missing API surface: config + max-redirects (round 1) | Fixed ✓ — max-redirects folded into RequestConfig (line 74) |
| Negative timeout documentation (round 1) | Fixed ✓ — doc says "0 (or negative) means no timeout" (lines 65-66, README line 60) |
Unused path binding (round 1) |
Fixed ✓ — removed from build-and-send (lines 257-261) |
All six pieces of prior feedback have been genuinely addressed.
Findings
1. Redirect deduplication — verified correct
Traced the delegation chain:
request-stream-(line 384): the single redirect looprequest-stream-with-max-redirects(line 467): constructsRequestConfig.init 0 0 max-redirects, delegatesrequest-stream(line 479): delegates torequest-stream-with-max-redirectswithdefault-max-redirectsrequest-stream-with-config(line 495): passes caller's config straight through
All paths converge on the single loop. The old two-loop duplication is gone.
2. Config ownership — correct ✓
request-stream- takes &RequestConfig (borrow). Field access uses @(RequestConfig.max-redirects config) to copy out Ints. The reference is only read, never stored. request-stream-with-max-redirects creates a local cfg and borrows it — the local lives long enough. No ownership issues.
3. connect always takes config — correct ✓
Lines 230-250: plain-HTTP path checks connect-timeout > 0 to choose between TcpStream.connect-timeout and TcpStream.connect. HTTPS path correctly skips connect-timeout (documented limitation). Both paths apply read-timeout via set-timeout. The when guard ensures zero/negative timeouts are no-ops.
4. Minor: body-request-with-config duplicates Content-Length logic
body-request (lines 560-563) and body-request-with-config (lines 597-600) have identical Content-Length insertion logic. If this ever changes, both need updating. Could be deduplicated by having body-request delegate to body-request-with-config with a default config, mirroring how request-stream delegates to request-stream-with-max-redirects. Not blocking — the code is correct and the duplication is only 2 lines.
5. README and docs accurate ✓
API table lists all 16 public functions including the new -with-config variants. All signatures match the implementation. RequestConfig docs correctly describe the three-argument init and the TLS timeout limitation.
Verdict: merge
All prior feedback addressed. The refactoring is clean — one redirect loop instead of two, config ownership is correct, naming is consistent. The only nitpick is the minor Content-Length duplication, which is not blocking.
Summary
Configtype withconnect-timeoutandread-timeoutfields (in seconds, 0 = no timeout)-with-configvariants for all public request functions:request-with-config,request-stream-with-config,get-with-config,post-with-config,put-with-config,del-with-config,patch-with-configConnection.set-timeoutas a dispatch helper over plain/TLS streamsTcpStream.connect-timeout(plain HTTP only —TlsStream.connectdoes not support a timeout parameter)set-timeouton bothTcpStreamandTlsStreamUsage
Design notes
&Config) so it can be reused across callsrequest-stream-with-configreuses the same config for each hopConfig.defaultcreates a zero-timeout config equivalent to the existing behaviourTests
Config.defaultfield valuesget-with-configandpost-with-configwith default configrequest-stream-with-configstreaming variant/delay/10with 2s timeout)Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.