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:
- 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.
- 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.
- 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.
Every failure path in
Clientreturns(Result.Error String)and the strings come from three different owners:"missing host in URL","redirect without Location header","redirect with empty Location header","too many redirects (max %d)","incomplete HTTP headers""Invalid URI: ..."System.error-text(i.e.strerror(errno)) for TCP,tls_error()/ the OpenSSL error string for TLS, andResponse.parse's"Malformed response: ..."from httpA caller that wants to retry has to tell three things apart that the string cannot express:
build-and-sendwrites the entire request before the first read (http-client.carp:273), so"incomplete HTTP headers"and everyResponse.parsefailure mean the request was delivered. A caller retrying those re-executes work the server already did — for an LLM endpoint, a billed generation.TcpStream.connectreturnssunchanged whengetaddrinfofails (socket 0.2.3src/tcp_stream.h:27) and the caller then reportsstrerror(errno)— butgetaddrinfodoes not seterrno, so the message is stale. Measured here againsthttp://no-such-host.invalid/: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
ClientErrorsum type (Uri,Dns,Connect,Tls,Send,Receive,Parse,Redirect), or at minimum an exportedClient.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.