-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: RFC 9457 compatible * fix: test content-type (resolve: discussion_r1936938314) * fix: validation as package var (resolve: discussion_r1936947547) * fix: handle response encode error (resolve discussion_r1936947554) * feat: expanding problem details * fix: adjust typo and handle mutex * fix: use default title based on status code * fix: mu.RLock for getters
- Loading branch information
Showing
15 changed files
with
580 additions
and
129 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
package httpsuite | ||
|
||
import ( | ||
"net/http" | ||
"sync" | ||
) | ||
|
||
const BlankUrl = "about:blank" | ||
|
||
var ( | ||
mu sync.RWMutex | ||
problemBaseURL = BlankUrl | ||
errorTypePaths = map[string]string{ | ||
"validation_error": "/errors/validation-error", | ||
"not_found_error": "/errors/not-found", | ||
"server_error": "/errors/server-error", | ||
"bad_request_error": "/errors/bad-request", | ||
} | ||
) | ||
|
||
// ProblemDetails conforms to RFC 9457, providing a standard format for describing errors in HTTP APIs. | ||
type ProblemDetails struct { | ||
Type string `json:"type"` // A URI reference identifying the problem type. | ||
Title string `json:"title"` // A short, human-readable summary of the problem. | ||
Status int `json:"status"` // The HTTP status code. | ||
Detail string `json:"detail,omitempty"` // Detailed explanation of the problem. | ||
Instance string `json:"instance,omitempty"` // A URI reference identifying the specific instance of the problem. | ||
Extensions map[string]interface{} `json:"extensions,omitempty"` // Custom fields for additional details. | ||
} | ||
|
||
// NewProblemDetails creates a ProblemDetails instance with standard fields. | ||
func NewProblemDetails(status int, problemType, title, detail string) *ProblemDetails { | ||
if problemType == "" { | ||
problemType = BlankUrl | ||
} | ||
if title == "" { | ||
title = http.StatusText(status) | ||
if title == "" { | ||
title = "Unknown error" | ||
} | ||
} | ||
return &ProblemDetails{ | ||
Type: problemType, | ||
Title: title, | ||
Status: status, | ||
Detail: detail, | ||
} | ||
} | ||
|
||
// SetProblemBaseURL configures the base URL used in the "type" field for ProblemDetails. | ||
// | ||
// This function allows applications using httpsuite to provide a custom domain and structure | ||
// for error documentation URLs. By setting this base URL, the library can generate meaningful | ||
// and discoverable problem types. | ||
// | ||
// Parameters: | ||
// - baseURL: The base URL where error documentation is hosted (e.g., "https://api.mycompany.com"). | ||
// | ||
// Example usage: | ||
// | ||
// httpsuite.SetProblemBaseURL("https://api.mycompany.com") | ||
// | ||
// Once configured, generated ProblemDetails will include a "type" such as: | ||
// | ||
// "https://api.mycompany.com/errors/validation-error" | ||
// | ||
// If the base URL is not set, the default value for the "type" field will be "about:blank". | ||
func SetProblemBaseURL(baseURL string) { | ||
mu.Lock() | ||
defer mu.Unlock() | ||
problemBaseURL = baseURL | ||
} | ||
|
||
// SetProblemErrorTypePath sets or updates the path for a specific error type. | ||
// | ||
// This allows applications to define custom paths for error documentation. | ||
// | ||
// Parameters: | ||
// - errorType: The unique key identifying the error type (e.g., "validation_error"). | ||
// - path: The path under the base URL where the error documentation is located. | ||
// | ||
// Example usage: | ||
// | ||
// httpsuite.SetProblemErrorTypePath("validation_error", "/errors/validation-error") | ||
// | ||
// After setting this path, the generated problem type for "validation_error" will be: | ||
// | ||
// "https://api.mycompany.com/errors/validation-error" | ||
func SetProblemErrorTypePath(errorType, path string) { | ||
mu.Lock() | ||
defer mu.Unlock() | ||
errorTypePaths[errorType] = path | ||
} | ||
|
||
// SetProblemErrorTypePaths sets or updates multiple paths for different error types. | ||
// | ||
// This allows applications to define multiple custom paths at once. | ||
// | ||
// Parameters: | ||
// - paths: A map of error types to paths (e.g., {"validation_error": "/errors/validation-error"}). | ||
// | ||
// Example usage: | ||
// | ||
// paths := map[string]string{ | ||
// "validation_error": "/errors/validation-error", | ||
// "not_found_error": "/errors/not-found", | ||
// } | ||
// httpsuite.SetProblemErrorTypePaths(paths) | ||
// | ||
// This method overwrites any existing paths with the same keys. | ||
func SetProblemErrorTypePaths(paths map[string]string) { | ||
mu.Lock() | ||
defer mu.Unlock() | ||
for errorType, path := range paths { | ||
errorTypePaths[errorType] = path | ||
} | ||
} | ||
|
||
// GetProblemTypeURL get the full problem type URL based on the error type. | ||
// | ||
// If the error type is not found in the predefined paths, it returns a default unknown error path. | ||
// | ||
// Parameters: | ||
// - errorType: The unique key identifying the error type (e.g., "validation_error"). | ||
// | ||
// Example usage: | ||
// | ||
// problemTypeURL := GetProblemTypeURL("validation_error") | ||
func GetProblemTypeURL(errorType string) string { | ||
mu.RLock() | ||
defer mu.RUnlock() | ||
if path, exists := errorTypePaths[errorType]; exists { | ||
return getProblemBaseURL() + path | ||
} | ||
|
||
return BlankUrl | ||
} | ||
|
||
// getProblemBaseURL just return the baseURL if it isn't "about:blank" | ||
func getProblemBaseURL() string { | ||
mu.RLock() | ||
defer mu.RUnlock() | ||
if problemBaseURL == BlankUrl { | ||
return "" | ||
} | ||
return problemBaseURL | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
package httpsuite | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func Test_SetProblemBaseURL(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
input string | ||
expected string | ||
}{ | ||
{ | ||
name: "Set valid base URL", | ||
input: "https://api.example.com", | ||
expected: "https://api.example.com", | ||
}, | ||
{ | ||
name: "Set base URL to blank", | ||
input: BlankUrl, | ||
expected: BlankUrl, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
SetProblemBaseURL(tt.input) | ||
assert.Equal(t, tt.expected, problemBaseURL) | ||
}) | ||
} | ||
} | ||
|
||
func Test_SetProblemErrorTypePath(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
errorKey string | ||
path string | ||
expected string | ||
}{ | ||
{ | ||
name: "Set custom error path", | ||
errorKey: "custom_error", | ||
path: "/errors/custom-error", | ||
expected: "/errors/custom-error", | ||
}, | ||
{ | ||
name: "Override existing path", | ||
errorKey: "validation_error", | ||
path: "/errors/new-validation-error", | ||
expected: "/errors/new-validation-error", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
SetProblemErrorTypePath(tt.errorKey, tt.path) | ||
assert.Equal(t, tt.expected, errorTypePaths[tt.errorKey]) | ||
}) | ||
} | ||
} | ||
|
||
func Test_GetProblemTypeURL(t *testing.T) { | ||
// Setup initial state | ||
SetProblemBaseURL("https://api.example.com") | ||
SetProblemErrorTypePath("validation_error", "/errors/validation-error") | ||
|
||
tests := []struct { | ||
name string | ||
errorType string | ||
expectedURL string | ||
}{ | ||
{ | ||
name: "Valid error type", | ||
errorType: "validation_error", | ||
expectedURL: "https://api.example.com/errors/validation-error", | ||
}, | ||
{ | ||
name: "Unknown error type", | ||
errorType: "unknown_error", | ||
expectedURL: BlankUrl, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
result := GetProblemTypeURL(tt.errorType) | ||
assert.Equal(t, tt.expectedURL, result) | ||
}) | ||
} | ||
} | ||
|
||
func Test_getProblemBaseURL(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
baseURL string | ||
expectedResult string | ||
}{ | ||
{ | ||
name: "Base URL is set", | ||
baseURL: "https://api.example.com", | ||
expectedResult: "https://api.example.com", | ||
}, | ||
{ | ||
name: "Base URL is about:blank", | ||
baseURL: BlankUrl, | ||
expectedResult: "", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
problemBaseURL = tt.baseURL | ||
assert.Equal(t, tt.expectedResult, getProblemBaseURL()) | ||
}) | ||
} | ||
} | ||
|
||
func Test_NewProblemDetails(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
status int | ||
problemType string | ||
title string | ||
detail string | ||
expectedType string | ||
}{ | ||
{ | ||
name: "All fields provided", | ||
status: 400, | ||
problemType: "https://api.example.com/errors/validation-error", | ||
title: "Validation Error", | ||
detail: "Invalid input", | ||
expectedType: "https://api.example.com/errors/validation-error", | ||
}, | ||
{ | ||
name: "Empty problem type", | ||
status: 404, | ||
problemType: "", | ||
title: "Not Found", | ||
detail: "The requested resource was not found", | ||
expectedType: BlankUrl, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
details := NewProblemDetails(tt.status, tt.problemType, tt.title, tt.detail) | ||
assert.Equal(t, tt.status, details.Status) | ||
assert.Equal(t, tt.title, details.Title) | ||
assert.Equal(t, tt.detail, details.Detail) | ||
assert.Equal(t, tt.expectedType, details.Type) | ||
}) | ||
} | ||
} |
Oops, something went wrong.