šļø discussion API design question: should a 407 from a proxy check be Ok or Err?
I'm working on a Rust SDK that has a small check() method for testing a proxy connection, and I'm not sure I'm using Result in the least surprising way here.
If the proxy replies with 407 Proxy Authentication Required, I currently return Ok(Check), not Err.
My reasoning is basically: the check itself worked) I connected to the proxy and got a real response back. For this method, the status/reason/headers are useful information.
Err is only for cases where I couldn't get a usable response at all - timeout, DNS failure, connection failure, malformed response, etc.
Roughly:
match proxy.check(connect)? {
check if check.status == 200 => {
// accepted
}
check if check.status == 407 => {
// proxy replied, auth rejected
}
check => {
// some other resp
}
}
The part that feels a bit weird is this:
let check = proxy.check(connect)?;
That kind of reads like "the proxy is fine", while what it really means is only "the proxy gave a usable response".
Would you expect 407 to be an Err here, or does treating a protocol-level refusal as a successful diagnostic result make sense? š
5
u/teerre 13h ago
Return Check where it failed would be surprising to me. Checking error codes manually is unidiomatic unless you're working a literal http library
``` enum ProxyError { Auth, Connection }
Result<Check, ProxyError> ```
Seems like the natural shape. The only reason to separate the error would be if from Check you can also authenticate, but that would be two calls and some nesting, probably not worth it
It also highly depends what are users expected to do with these errors. The more actionable the error is, the larger its api can be
3
u/owbrooly 16h ago
It s a bit tricky. I m guessing you are working on http proxy, in which case 407 when no auth is supplied indicates that proxy requires auth. But if authentication was supplied it indicates that auth failed. Both errors are user facing, so I guess best way to handle this is to return Enum in Ok() branch that minimally represents current state
2
u/GG_Men 15h ago
Thatās a good point. I hadnāt really separated āauth requiredā from āauth failedā when thinking about the 407 case.
Keeping protocol responses in Ok() still feels right to me, but maybe an outcome enum inside Check could make that distinction explicit without changing the outer Result2
u/owbrooly 15h ago
Yeah, that feels right. Err is more of something went wrong along the way in this case. At least it s how I would feel about it
1
u/South_Survey_2088 10h ago
I would argue that the function name is the issue here. "check()" leaves a lot of ambiguity and reads more like a boolean-like. I am not a web dev so there might be a better name, but something like status() would make it clearer that the result is a state that should be inspected rather than a condition.
1
u/Lucretiel Datadog 10h ago
Often for cases like this I start wrapping Results in each other. For example, in low level network protocol code, I often end up with Result<Result<Response, ProtocolError>, TcpError>. Iād recommend something like that.
That being said, I think youāre right to consider this design space. The primary advantage of structured errors and errors-as-values is that you can react to them in granular ways; you should absolutely not feel obligated to unthinkingly propagate all errors to callers. If a 407 code semantically represents success for the particular operation youāre attempting, then by all means return it as an Ok.Ā
1
u/maccam94 8h ago
The one reason I'd consider making this an error is for ? ergonomics. Then happy path just gets the value, and anything else propagates up to the error handling/retry logic.
1
u/neon_lodger 7h ago
agreed on the naming issue. If you call it status() and return a Result<Status, ConnectionError>, then 407 is clearly Ok(Status::NeedsAuth). This separates "the proxy is dead" from "the proxy is talking to me but rejecting my credentials," which keeps your ? operator honest since a hard fail still triggers early exit.
1
u/stevecooperorg 6h ago
generally I'd put;
- OK as "we had the conversation" even if we don't like the answer.
- Err as "we couldn't connect / there was a timeout / etc"
The problem is naturally two-stage -- make the request / interpret the response. Return OK if you made the request. Handle "I don't like it" later. So;
match request()? {
Ok(code) => match code {
NOT_FOUND => {
// seems bad, right? but we successfully
// asked the question; we just don't like the answewr.
11
u/BobTreehugger 16h ago
Both are valid depending on the use case. Since you're returning the check struct that has the http status, that's probably fine.
Not sure if you want to do it, but a nice thing about Result is that you can nest them --
Result<Result<Check, AuthError>, ConnectionError>is a valid thing to do, but you know more about the intended use of this than I do.If this is mostly used to check if you have a valid connection, your original design seems better, if you mostly want to check if you can actually auth and go through the proxy, seems like you want error if you can't auth.
Looks to me like you were able to check, not what the result of the check is.