carpentry-org / carpentry-org/http-client
Classify transport errors instead of returning free-form strings
- Dominant language
- Python
- Stars
- 2
- Forks
- 0
- Avg merge
- 1d 2m
- Merged PRs (30d)
- 6
Description
Every failure path in `Client` returns `(Result.Error String)` and the strings come from three different owners:
- http-client prose: `"missing host in URL"`, `"redirect without Location header"`, `"redirect with empty Location header"`, `"too many redirects (max %d)"`, `"incomplete HTTP headers"`
- uri prose: `"Invalid URI: ..."`
- whatever the transport hands back: `System.error-text` (i.e. `strerror(errno)`) for TCP, `tls_error()` / the OpenSSL error string for TLS, and `Response.parse`'s `"Malformed response: ..."` from http
A caller that wants to retry has to tell three things apart that the string cannot express:
1. **Retryable vs permanent.** A refused or reset connection is worth another attempt; a malformed URL, an unfollowable redirect chain or a certificate that does not verify is not. Today that is prefix-matching English prose, which silently reclassifies on a wording change.
2. **Connect-stage vs post-send.** `build-and-send` writes the entire request before the first read (http-client.carp:273), so `"incomplete HTTP headers"` and every `Response.parse` failure mean the request *was* delivered. A caller retrying those re-executes work the server already did — for an LLM endpoint, a billed generation.
3. **DNS failures at all.** `TcpStream.connect` returns `s` unchanged when `getaddrinfo` fails (socket 0.2.3 `src/tcp_stream.h:27`) and the caller then reports `strerror(errno)` — but `getaddrinfo` does not set `errno`, so the message is stale. Measured here against `http://no-such-host.invalid/`:
```
DNS-ERR: [Invalid argument]
```
That is EINVAL left over from earlier in the process, not a DNS error, and it is indistinguishable from a genuine EINVAL.
Something like a `ClientError` sum type (`Uri`, `Dns`, `Connect`, `Tls`, `Send`, `Receive`, `Parse`, `Redirect`), or at minimum an exported `Client.retryable-error?` predicate that owns the classification next to the code that produces the strings, would let downstream retry loops be correct rather than approximately correct.
Downstream: carpentry-org/llm#19 currently keeps a prefix list of these strings and a comment pointing here.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with build-and-send in http-client.carp:273 and trace its request, read, and Response.parse failure paths. Inspect TcpStream.connect in src/tcp_stream.h:27, including the getaddrinfo failure, then compare the URI, redirect, TLS, and transport producers named in the issue. Done means callers can distinguish error kind, retryability, and whether the request was sent, with DNS failures represented accurately.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, networking
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100