Skip to content

Configurable server-level error handler for request errors #198

@rcardin

Description

@rcardin

Currently, request error handling is inconsistent:

  • Path/query parameter errors (PathParamError, QueryParamError) are automatically caught in Route.matches and hardcoded to 400 Bad Request responses
  • Body decoding errors (DecodingError from req.as[A]) must be manually handled by users via Raise.recover in every route handler

This forces boilerplate in every route that decodes a request body:

POST(p"/users") { req =>
  Raise.recover {
    val user = req.as[User]
    Response.created(user)
  } {
    case e: DecodingError => Response.badRequest(e.message)
    case ConnectionError  => Response.internalServerError("...")
  }
}

Additionally, the hardcoded error response format cannot be customized: Many projects use standardized error formats (e.g., RFC 7807 Problem Details) or custom JSON error envelopes.

Proposal

Introduce a configurable server-level error handler that unifies handling of all request-level errors (PathParamError, QueryParamError, DecodingError) with a sensible default.

New types:

  • RequestError — enum wrapping all three error types (Path, Query, Body cases)
  • ErrorHandler — type alias RequestError => Response with a default (400 Bad Request)

API changes:

  • ServerDef gains an onError builder method for setting the error handler
  • Route handler functions gain Raise[DecodingError] context so req.as[A] works without manual wrapping
  • Route.matches and Routes.handle delegate to the configurable error handler instead of hardcoding responses

After:

// DecodingError propagates to server error handler — no manual wrapping
POST(p"/users") { req =>
  val user = req.as[User]
  Response.created(user)
}

// Custom error format at server level
val server = YaesServer.route(routes*).onError {
  case RequestError.Body(e)  => Response.badRequest(s"""{"error":"${e.message}"}""")
  case RequestError.Path(e)  => Response.badRequest(s"""{"error":"${e.message}"}""")
  case RequestError.Query(e) => Response.badRequest(s"""{"error":"${e.message}"}""")
}

Per-route override still works via Raise.recover inside the handler when needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions