-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
net: add UnwrapErr field and Unwrap method to *DNSError #63116
Comments
|
AFAIK the
I don't know whether we want to allow this (we would have to enforce somehow that There is still an issue (with the current proposal), that someone might change the I am unsure about this (in either way). |
Since all the existing fields of |
Here is a test for #63109 that I wrote last night. func TestCancelBeforeDial(t *testing.T) {
ln := newLocalListener(t, "tcp")
defer ln.Close()
_, port, err := SplitHostPort(ln.Addr().String())
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
var d Dialer
_, err = d.DialContext(ctx, "tcp", JoinHostPort("localhost", port))
if err == nil {
t.Error("DialContext with canceled context did not fail")
} else if !errors.Is(err, context.Canceled) {
t.Errorf("error.Is(%v, context.Canceled) failed", err)
}
} |
Fine, let's make the error field exported. The proposal looks like this now: // DNSError represents a DNS lookup error.
type DNSError struct {
// Underlying is an error that caused this DNSError.
// It is returned by the Unwrap method.
//
// When updating this field make sure to update
// the Err field accordingly, so that the Error
// and Unwrap methods don't diverge.
Underlying error
// Err is a description of the error, for backwards compatibility
// the net package sets this error to the value
// returned by Underlying.Error(), it used by
// the Error method to produce the error string.
Err string
// (...)
}
// Unwrap returns e.Underlying.
func (e *DNSError) Unwrap() error { return e.Underlying } |
Having an exported error may also come in handy with possible future support for "extended dns errors", see https://datatracker.ietf.org/doc/html/rfc8914. Could be good to take that into account. It is useful to learn about specifics of dns failures, especially with dnssec errors, and for getting a more descriptive message than "servfail". |
@mjl- Great idea! I made #63192 for this. This should allow for detailed error messages from the pure go resolver. Not sure how the exported error helps with this. I assume you mean an exported (new) error type just for extended dns errors, I don't think we will ever do this it would be impossible to support this on windows and on unix cgo resolver. |
I assume you mean an exported (new) error type just for extended dns errors, I don't think we will ever do this it would be impossible to support this on windows and on unix cgo resolver.
Yes, a new error type for extended dns errors is what I had in mind.
I would think windows and unix c would at some point make these extended dns errors accessible, perhaps with a new API. They'll want it sooner or later.
We could make these detailed errors optional. DNS servers (recursive resolvers) don't have to support EDE, and not all do, so there is already no guarantee you'll get them.
Anyway, if underlying errors can be unwrapped, and I can set those errors, I could make a new package that can return a net.DNSError with specific EDE errors of my own package. It would make it easier to provide a drop-in replacement resolver package.
|
I implemented this change in CL 532217, it also simplifies the code a bit with an internal constructor for the *DNSError, so that we don't have to set the Edit: this also implies that the net package will always construct |
Change https://go.dev/cl/532217 mentions this issue: |
@rsc Any chance of getting this through proposal process? This is a bug-fix that needs a proposal. It would be nice to fix this for go 1.23. Thanks |
I thought when we added Unwrap we explicitly looked at all types with an Err field and decided which to do and not do. For net.AddrError, we explicitly decided against it. For net.DNSError, there is no Err field currently, so we could add one. Except there is an 'Err string' field. There are lots of times when errors should not be wrapped. I don't see a discussion here of which errors to expose with Unwrap and why that is correct. |
What kind of errors shouldn't be wrapped? I think that we should wrap all errors and just for backwards-compatibility set the |
Surveying existing
So I support adding (It's been a while, but I believe the only reason we didn't add an |
I disagree about at least the syscall return. I think wrapping is harmful, because it exposes the underlying implementation in ways that are easier to depend on. Most syscall errors are also highly non-specific. I can see wanting Unwrap to work for the context error. I think that if we proceed, we should default to Underlying==nil and Err != "" and only set Underlying for the specific cases that we want Unwrap to work for (like context). I'm also not convinced about Underlying; perhaps it should be UnwrapErr. |
This proposal has been added to the active column of the proposals project |
Underlying sounds better to me |
Underlying is a concept in go/types; it is never used for errors in API names. |
Have all remaining concerns about this proposal been addressed? The proposal is to add |
Based on the discussion above, this proposal seems like a likely accept. The proposal is to add |
FYI, currently the net package does not return directly errors from the Line 81 in c9ed561
Lines 427 to 438 in c9ed561
Do we want to keep it this way? |
We should keep it that for now. Changing it seems likely to break existing code (which is we do the mapping in the first place). If somebody wants to investigate changing this behavior, that can be a separate proposal. |
How do we want to handle errors from Should we map the context errors, with Lines 176 to 179 in 68d3a9e
We might also do something like: if errors.Is(err, context.Canceled) {
return &DnsError{
Err: err.Error(),
UnwrapErr: errCanceled, // same error as returned by mapErr
}
} I think that if we do not expose all errors, then the third approach is the best. |
Sorry, I'm not sure exactly what you are asking. If we returned a mapped error today, we should continue to do that. Note that the mapped errors already return true for things like |
We currently do not map errors returned by Lines 176 to 179 in 68d3a9e
How we should handle errors returned from the Do we want to return the error as is. c, err := r.dial(ctx, network, server)
if err != nil {
return &DnsError{
Err: err.Error(),
UnwrapErr: err,
}
} Return the error, but only when it matches c, err := r.dial(ctx, network, server)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return &DnsError{
Err: err.Error(),
UnwrapErr: err,
}
}
return &DnsError{
Err: err.Error(),
}
} Or return only c, err := r.dial(ctx, network, server)
if err != nil {
if errors.Is(err, context.Canceled) {
return &DnsError{
Err: err.Error(),
UnwrapErr: errCanceled,
}
}
if errors.Is(err, context.DeadlineExceeded) {
return &DnsError{
Err: err.Error(),
UnwrapErr: errTimeout,
}
}
return &DnsError{
Err: err.Error(),
}
} |
Looking at |
Oh, right I somehow missed that, but the mapping is obviously not great, it does not go through Unwrap errors, so it might lead to some confusing cases: r := net.Resolver{
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
return nil, context.Canceled
},
}
_, err := r.LookupMX(context.Background(), "test")
fmt.Println(err) // (*DnsError).UnwrapErr = errCancaled r := net.Resolver{
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
return nil, &net.OpError{Err: context.Canceled} // wrapped error
},
}
_, err := r.LookupMX(context.Background(), "test")
fmt.Println(err) // (*DnsError).UnwrapErr = &net.OpError{Err: context.Canceled}
The current status of this proposal is to only expose context errors, so we should not expose all errors from So: Lines 305 to 311 in 68d3a9e
becomes: p, h, err := r.exchange(ctx, server, q, cfg.timeout, cfg.useTCP, cfg.trustAD)
if err != nil {
dnsErr := &DNSError{
Err: err.Error(),
Name: name,
Server: server,
}
if errors.Is(err, context.Canceled) {
dnsErr.UnwrapErr = errCalceled
}
if errors.Is(err, context.DeadlineExceeded) {
dnsErr.UnwrapErr = errTimeout
}
// (...)
} This way we do not expose implementation errors and allow the |
That is OK with me. I guess the only question is how we should handle an error returned by a system call: should we wrap it or not? Since we haven't wrapped it in the past, I'm fine with continuing to not wrap it, at least until we have a clear reason to do so. Thanks. |
It sounds like the only use of Unwrap will be for Is, in which case we could decide that instead of adding an Unwrap method we could implement an Is method on DNSError. Is there a reason to prefer one or the other? Probably Unwrap is more general for dealing with future issues? |
Just as you mentioned, |
|
No change in consensus, so accepted. 🎉 The proposal is to add |
Edit: current proposal: #63116 (comment)
This is a proposal to fix the #63109 issue.
I propose adding a
causeErr
field toDNSError
and anUnwrap
method that returns thecauseErr
.This way we keep backwards compatibility for the
Error
method, we always construct the error fromthe
Err string
field (we keep the current implementation). In the net pacakge we will then makeErr
field to be the result of the first call tocauseErr.Error()
.The text was updated successfully, but these errors were encountered: