carpentry-org / carpentry-org/http-client

Classify transport errors instead of returning free-form strings

Open
#24 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.