guardrail-dev / guardrail-dev/guardrail

Support for All HTTP Errors without defining them individually

Open
#273 3 comments 0 reactions 0 assignees View on GitHub
question
Dominant language
Scala
Stars
541
Forks
138
PR merge metrics
No merged PRs in 30d

Description

I'd like to see a way to define an HTTP route that will include the possibility of all HTTP status codes in the response.

Proposal: include an `errors` key that covers every 4xx and 5xx status code

```
/v1/MySweetRoute:
get:
operationId: ListStuff
x-scala-package: Data
x-scala-tracing-label: "v1-get-sweet-data"
produces:
- "application/json"
parameters:
- name: param
in: query
type: string
responses:
200:
description: "SweetData instance"
schema:
$ref: '#/definitions/SweetData'
errors:
description: Bad Request
schema:
$ref: "#/definitions/ApiError"
```
Also the generated classes for the 4xx and 5xx error codes should all extend a `sealed trait` common to all routes. Note this doesn't list every 4xx/5xx error just to keep it short
```
trait AllErrors[T] {
def BadRequest(value: ApiError): T
def Forbidden(value: ApiError): T
def NotFound(value: ApiError): T
def RequestTimeout(value: ApiError): T
def Conflict(value: ApiError): T
def UnsupportedMediaType(value: ApiError): T
def UnprocessableEntity(value: ApiError): T
def TooManyRequests(value: ApiError): T
def InternalServerError(value: ApiError): T
def ServiceUnavailable(value: ApiError): T
}
```

And each of the routes that utilize the `errors` setting will have the `object` for that route extend this trait. So above i use `operation-id: ListStuff`, there will be:
```
object ListStuffResponse extends AllErrors[ListStuffResponse]
```

This allows you to write errors handlers common to all routes. This helps keep success handling logic route-specific and error handling logic route-independent.

Current way of doing this (note this also doesn't include all the 4xx and 5xx just to keep it short)
```
/v1/MySweetRoute:
get:
operationId: ListStuff
x-scala-package: Data
x-scala-tracing-label: "v1-get-sweet-data"
produces:
- "application/json"
parameters:
- name: param
in: query
type: string
responses:
200:
description: "SweetData instance"
schema:
$ref: '#/definitions/SweetData'
400:
description: Bad Request
schema:
$ref: "#/definitions/ApiError"
403:
description: Forbidden
schema:
$ref: "#/definitions/ApiError"
404:
description: Not Found
schema:
$ref: "#/definitions/ApiError"
408:
description: Request Timeout
schema:
$ref: "#/definitions/ApiError"
409:
description: New status not valid
schema:
$ref: "#/definitions/ApiError"
415:
description: Unsupported Media Type
schema:
$ref: "#/definitions/ApiError"
422:
description: Unprocessable Entity
schema:
$ref: "#/definitions/ApiError"
429:
description: Too Many Requests
schema:
$ref: "#/definitions/ApiError"
500:
description: Internal Server Error
schema:
$ref: "#/definitions/ApiError"
503:
description: Service Unavailable
schema:
$ref: "#/definitions/ApiError"
```

and then in your code you would need
```
type GenericResponse[T] = {
def BadRequest(value: ApiError): T
def Forbidden(value: ApiError): T
def NotFound(value: ApiError): T
def RequestTimeout(value: ApiError): T
def Conflict(value: ApiError): T
def UnsupportedMediaType(value: ApiError): T
def UnprocessableEntity(value: ApiError): T
def TooManyRequests(value: ApiError): T
def InternalServerError(value: ApiError): T
def ServiceUnavailable(value: ApiError): T
}

// ServiceError is a common ancestor in the error class hierarchy used service-wide
// that is an internal model wthich corresponds to the public ApiError definition
def serviceErrorToRouteSpecificError[T](respond: GenericResponse[T], err: ServiceError): T = {
val newErrorResponse = serviceErrorToGuardrailError(err)
err.statusCode.intValue match {
case 400 => respond.BadRequest(newErrorResponse)
case 403 => respond.Forbidden(newErrorResponse)
case 404 => respond.NotFound(newErrorResponse)
case 408 => respond.RequestTimeout(newErrorResponse)
case 409 => respond.Conflict(newErrorResponse)
case 415 => respond.UnsupportedMediaType(newErrorResponse)
case 422 => respond.UnprocessableEntity(newErrorResponse)
case 429 => respond.TooManyRequests(newErrorResponse)
case 500 => respond.InternalServerError(newErrorResponse)
case 503 => respond.ServiceUnavailable(newErrorResponse)
case _ => respond.BadRequest(newErrorResponse)
}
}

```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.