Skip to content

Commit 6dc6ede

Browse files
authored
Add server error handler (#31)
1 parent c030046 commit 6dc6ede

74 files changed

Lines changed: 6146 additions & 2048 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,19 @@ Documentation is in `docs/` using MkDocs. Key files:
253253
2. Add to `nav:` section in `mkdocs.yml`
254254
3. For extensions, create in `docs/extensions/` and add under Extensions nav
255255

256+
### Code snippets in docs
257+
Documentation uses MkDocs snippets to include code from example files. The format is:
258+
```
259+
--8<-- "path/to/file.go:start_line:end_line"
260+
```
261+
For example: `--8<-- "extensions/xgotype/gen.go:11:14"` includes lines 11-14 from that file.
262+
263+
**After regenerating examples**, verify that line number references in docs are still correct:
264+
```bash
265+
grep -rn '\-\-8<\-\-' docs/ | grep -E ':[0-9]+:[0-9]+'
266+
```
267+
Then check each referenced file to ensure the line ranges still point to the expected code.
268+
256269
### Local preview
257270
```bash
258271
pip install mkdocs-material

configuration-schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,10 @@
268268
"type": "string",
269269
"description": "Package alias to prefix model types with. Used when models are generated separately (generate.models: false). Example: 'types' will generate 'types.User' instead of 'User'."
270270
},
271+
"multipart-max-memory": {
272+
"type": "integer",
273+
"description": "Maximum memory in MB for multipart form parsing. Defaults to 32MB. Files exceeding this are stored in temp files."
274+
},
271275
"validation": {
272276
"$ref": "#/definitions/HandlerValidation",
273277
"description": "Validation options for request/response validation in handlers."

docs/configuration.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,18 @@ generate:
250250

251251
This generates `types.User` instead of `User` in the handler code.
252252

253+
#### `generate.handler.multipart-max-memory`
254+
**Type:** `integer` | **Default:** `32`
255+
256+
Maximum memory in MB for multipart form parsing. Files exceeding this limit are stored in temporary files on disk.
257+
258+
```yaml
259+
generate:
260+
handler:
261+
kind: chi
262+
multipart-max-memory: 64
263+
```
264+
253265
#### `generate.handler.validation.request`
254266
**Type:** `boolean` | **Default:** `false`
255267

@@ -463,7 +475,50 @@ error-mapping:
463475
UpdateClientErrorResponseJSON: arrayField[].code
464476
```
465477

466-
When configured, the response type will have an `Error() string` method that returns the value from the specified field.
478+
When configured, the response type will have:
479+
480+
1. **`Error() string` method** - Returns the value from the specified field path
481+
2. **Constructor function** - `NewTypeName(message string)` for easy error creation
482+
483+
### Generated Code Example
484+
485+
Given this configuration:
486+
487+
```yaml
488+
error-mapping:
489+
InvalidRequestError: error.message
490+
```
491+
492+
The generator produces:
493+
494+
```go
495+
type InvalidRequestError struct {
496+
ErrorData *ErrorData `json:"error,omitempty"`
497+
}
498+
499+
func (i InvalidRequestError) Error() string {
500+
res0 := i.ErrorData
501+
if res0 == nil {
502+
return "unknown error"
503+
}
504+
return res0.Message
505+
}
506+
507+
func NewInvalidRequestError(message string) InvalidRequestError {
508+
return InvalidRequestError{ErrorData: &ErrorData{Message: message}}
509+
}
510+
```
511+
512+
The constructor is useful in server implementations for returning typed errors:
513+
514+
```go
515+
func (s *Service) CreateUser(ctx context.Context, opts *CreateUserOpts) (*CreateUserResponse, error) {
516+
if opts.Body.Email == "" {
517+
return nil, NewInvalidRequestError("email is required")
518+
}
519+
// ...
520+
}
521+
```
467522

468523
See [examples/client/example1/cfg.yaml](https://github.com/doordash-oss/oapi-codegen-dd/blob/main/examples/client/example1/cfg.yaml){:target="_blank"} for a complete example.
469524

docs/server-generation.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,143 @@ func TestGetUser(t *testing.T) {
370370
}
371371
```
372372

373+
## Error Handling
374+
375+
The generated code includes a flexible error handling system that separates error classification from error response formatting.
376+
377+
### Error Types
378+
379+
The `HTTPAdapter` handles four types of errors:
380+
381+
| Error Kind | Description | Default Status |
382+
|------------|-------------|----------------|
383+
| `OapiErrorKindParse` | Parameter parsing errors (invalid path/query/header) | 400 |
384+
| `OapiErrorKindDecode` | Request body decoding errors (invalid JSON, form data) | 400 |
385+
| `OapiErrorKindValidation` | Request validation errors (failed schema validation) | 400 |
386+
| `OapiErrorKindService` | Service/business logic errors from your implementation | 500 (or typed) |
387+
388+
### Default Behavior
389+
390+
The `OapiDefaultErrorHandler` respects the `Accept` header:
391+
392+
- **JSON** (`application/json`, `*/*`, or empty): Returns JSON response
393+
- **Other**: Returns plain text
394+
395+
```go
396+
// JSON error response for parse/decode/validation errors
397+
{
398+
"error": "invalid parameter \"id\": strconv.Atoi: parsing \"abc\": invalid syntax"
399+
}
400+
```
401+
402+
Service errors are JSON-encoded directly, so the response structure matches your error type's JSON tags.
403+
404+
### Custom Error Handler
405+
406+
Implement the `OapiErrorHandler` interface to customize error responses, add logging, or collect metrics:
407+
408+
```go
409+
type OapiErrorHandler interface {
410+
HandleError(w http.ResponseWriter, r *http.Request, statusCode int, err error)
411+
}
412+
```
413+
414+
The `err` parameter is either:
415+
416+
- **`OapiHandlerError`** - A generic handler error (parse, decode, validation) with context fields
417+
- **Typed error** - An error type from your OpenAPI spec (when `error-mapping` is configured)
418+
419+
```go
420+
type OapiHandlerError struct {
421+
Kind OapiErrorKind // Type of error (Parse, Decode, Validation, Service)
422+
OperationID string // OpenAPI operation ID (e.g., "GetUser", "CreateOrder")
423+
Message string // Error message
424+
ParamName string // Parameter name (for parse errors)
425+
ParamLocation string // Parameter location: "path", "query", "header" (for parse errors)
426+
}
427+
```
428+
429+
Example custom handler with logging:
430+
431+
```go
432+
type LoggingErrorHandler struct {
433+
logger *slog.Logger
434+
}
435+
436+
func (h *LoggingErrorHandler) HandleError(w http.ResponseWriter, r *http.Request, statusCode int, err error) {
437+
// Check if it's a generic handler error
438+
if handlerErr, ok := err.(api.OapiHandlerError); ok {
439+
h.logger.Error("request error",
440+
"operation", handlerErr.OperationID,
441+
"kind", handlerErr.Kind,
442+
"error", handlerErr.Message,
443+
"status", statusCode,
444+
"param", handlerErr.ParamName,
445+
"param_location", handlerErr.ParamLocation,
446+
)
447+
} else {
448+
// Typed error from OpenAPI spec or service error
449+
h.logger.Error("service error",
450+
"error", err.Error(),
451+
"status", statusCode,
452+
)
453+
}
454+
455+
// Write response
456+
w.Header().Set("Content-Type", "application/json")
457+
w.WriteHeader(statusCode)
458+
459+
if handlerErr, ok := err.(api.OapiHandlerError); ok {
460+
json.NewEncoder(w).Encode(map[string]string{
461+
"error": handlerErr.Message,
462+
})
463+
} else {
464+
// Typed error - encode directly to match API contract
465+
json.NewEncoder(w).Encode(err)
466+
}
467+
}
468+
```
469+
470+
Use your custom handler with `WithErrorHandler`:
471+
472+
```go
473+
svc := api.NewService()
474+
handler := api.NewRouter(svc,
475+
api.WithErrorHandler(&LoggingErrorHandler{logger: slog.Default()}),
476+
)
477+
```
478+
479+
### Typed Error Responses
480+
481+
When your OpenAPI spec defines error response types and you configure `error-mapping`, the generator creates typed errors with constructors:
482+
483+
```yaml
484+
# cfg.yaml
485+
error-mapping:
486+
InvalidRequestError: error.message
487+
```
488+
489+
This generates:
490+
491+
```go
492+
func NewInvalidRequestError(message string) InvalidRequestError {
493+
return InvalidRequestError{ErrorData: &ErrorData{Message: message}}
494+
}
495+
```
496+
497+
Use in your service implementation:
498+
499+
```go
500+
func (s *Service) CreateUser(ctx context.Context, opts *CreateUserOpts) (*CreateUserResponse, error) {
501+
if opts.Body.Email == "" {
502+
return nil, NewInvalidRequestError("email is required")
503+
}
504+
// ...
505+
}
506+
```
507+
508+
The `HTTPAdapter` automatically detects typed errors and uses the appropriate status code from your OpenAPI spec.
509+
373510
## Examples
374511

375512
Complete examples for each framework are available in the repository:

examples/client/example1/example1/responses.go

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/filtering/by-property/gen.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/responses/error-mapping/ex1/gen.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/responses/error-mapping/from-aliased/gen.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/responses/error-mapping/from-array/gen.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)