Implement suberrors & subproblems (#4227)

Updates #4193

Updating relevant Boulder locations to use WithSubErrors and WithSubProblems will be done in a separate follow-up PR.
This commit is contained in:
Daniel McCarney 2019-05-23 22:41:55 -04:00 committed by Roland Bracewell Shoemaker
parent 4d40cf58e4
commit ecd1ea6c61
4 changed files with 150 additions and 3 deletions

View File

@ -1,6 +1,10 @@
package errors
import "fmt"
import (
"fmt"
"github.com/letsencrypt/boulder/identifier"
)
// ErrorType provides a coarse category for BoulderErrors
type ErrorType int
@ -24,14 +28,32 @@ const (
// BoulderError represents internal Boulder errors
type BoulderError struct {
Type ErrorType
Detail string
Type ErrorType
Detail string
SubErrors []SubBoulderError
}
// SubBoulderError represents sub-errors specific to an identifier that are
// related to a top-level internal Boulder error.
type SubBoulderError struct {
*BoulderError
Identifier identifier.ACMEIdentifier
}
func (be *BoulderError) Error() string {
return be.Detail
}
// WithSubErrors returns a new BoulderError instance created by adding the
// provided subErrs to the existing BoulderError.
func (be *BoulderError) WithSubErrors(subErrs []SubBoulderError) *BoulderError {
return &BoulderError{
Type: be.Type,
Detail: be.Detail,
SubErrors: append(be.SubErrors, subErrs...),
}
}
// New is a convenience function for creating a new BoulderError
func New(errType ErrorType, msg string, args ...interface{}) error {
return &BoulderError{

50
errors/errors_test.go Normal file
View File

@ -0,0 +1,50 @@
package errors
import (
"testing"
"github.com/letsencrypt/boulder/identifier"
"github.com/letsencrypt/boulder/test"
)
// TestWithSubErrors tests that a boulder error can be created by adding
// suberrors to an existing top level boulder error
func TestWithSubErrors(t *testing.T) {
topErr := &BoulderError{
Type: RateLimit,
Detail: "don't you think you have enough certificates already?",
}
subErrs := []SubBoulderError{
SubBoulderError{
Identifier: identifier.DNSIdentifier("example.com"),
BoulderError: &BoulderError{
Type: RateLimit,
Detail: "everyone uses this example domain",
},
},
SubBoulderError{
Identifier: identifier.DNSIdentifier("what about example.com"),
BoulderError: &BoulderError{
Type: RateLimit,
Detail: "try a real identifier value next time",
},
},
}
outResult := topErr.WithSubErrors(subErrs)
// The outResult should be a new, distinct error
test.AssertNotEquals(t, topErr, outResult)
// The outResult error should have the correct sub errors
test.AssertDeepEquals(t, outResult.SubErrors, subErrs)
// Adding another suberr shouldn't squash the original sub errors
anotherSubErr := SubBoulderError{
Identifier: identifier.DNSIdentifier("another ident"),
BoulderError: &BoulderError{
Type: RateLimit,
Detail: "another rate limit err",
},
}
outResult = outResult.WithSubErrors([]SubBoulderError{anotherSubErr})
test.AssertDeepEquals(t, outResult.SubErrors, append(subErrs, anotherSubErr))
}

View File

@ -3,6 +3,8 @@ package probs
import (
"fmt"
"net/http"
"github.com/letsencrypt/boulder/identifier"
)
// Error types that can be used in ACME payloads
@ -40,12 +42,34 @@ type ProblemDetails struct {
// HTTPStatus is the HTTP status code the ProblemDetails should probably be sent
// as.
HTTPStatus int `json:"status,omitempty"`
// SubProblems are optional additional per-identifier problems. See
// RFC 8555 Section 6.7.1: https://tools.ietf.org/html/rfc8555#section-6.7.1
SubProblems []SubProblemDetails `json:"subproblems,omitempty"`
}
// SubProblemDetails represents sub-problems specific to an identifier that are
// related to a top-level ProblemDetails.
// See RFC 8555 Section 6.7.1: https://tools.ietf.org/html/rfc8555#section-6.7.1
type SubProblemDetails struct {
ProblemDetails
Identifier identifier.ACMEIdentifier
}
func (pd *ProblemDetails) Error() string {
return fmt.Sprintf("%s :: %s", pd.Type, pd.Detail)
}
// WithSubProblems returns a new ProblemsDetails instance created by adding the
// provided subProbs to the existing ProblemsDetail.
func (pd *ProblemDetails) WithSubProblems(subProbs []SubProblemDetails) *ProblemDetails {
return &ProblemDetails{
Type: pd.Type,
Detail: pd.Detail,
HTTPStatus: pd.HTTPStatus,
SubProblems: append(pd.SubProblems, subProbs...),
}
}
// statusTooManyRequests is the HTTP status code meant for rate limiting
// errors. It's not currently in the net/http library so we add it here.
const statusTooManyRequests = 429

View File

@ -5,6 +5,7 @@ import (
"net/http"
"github.com/letsencrypt/boulder/identifier"
"github.com/letsencrypt/boulder/test"
)
@ -77,5 +78,55 @@ func TestProblemDetailsConvenience(t *testing.T) {
if c.pb.Detail != c.detail {
t.Errorf("Incorrect detail message. Expected %s got %s", c.detail, c.pb.Detail)
}
if subProbLen := len(c.pb.SubProblems); subProbLen != 0 {
t.Errorf("Incorrect SubProblems. Expected 0, found %d", subProbLen)
}
}
}
// TestWithSubProblems tests that a new problem can be constructed by adding
// subproblems.
func TestWithSubProblems(t *testing.T) {
topProb := &ProblemDetails{
Type: RateLimitedProblem,
Detail: "don't you think you have enough certificates already?",
HTTPStatus: statusTooManyRequests,
}
subProbs := []SubProblemDetails{
SubProblemDetails{
Identifier: identifier.DNSIdentifier("example.com"),
ProblemDetails: ProblemDetails{
Type: RateLimitedProblem,
Detail: "don't you think you have enough certificates already?",
HTTPStatus: statusTooManyRequests,
},
},
SubProblemDetails{
Identifier: identifier.DNSIdentifier("what about example.com"),
ProblemDetails: ProblemDetails{
Type: MalformedProblem,
Detail: "try a real identifier value next time",
HTTPStatus: http.StatusConflict,
},
},
}
outResult := topProb.WithSubProblems(subProbs)
// The outResult should be a new, distinct problem details instance
test.AssertNotEquals(t, topProb, outResult)
// The outResult problem details should have the correct sub problems
test.AssertDeepEquals(t, outResult.SubProblems, subProbs)
// Adding another sub problem shouldn't squash the original sub problems
anotherSubProb := SubProblemDetails{
Identifier: identifier.DNSIdentifier("another ident"),
ProblemDetails: ProblemDetails{
Type: RateLimitedProblem,
Detail: "yet another rate limit err",
HTTPStatus: statusTooManyRequests,
},
}
outResult = outResult.WithSubProblems([]SubProblemDetails{anotherSubProb})
test.AssertDeepEquals(t, outResult.SubProblems, append(subProbs, anotherSubProb))
}