-
Notifications
You must be signed in to change notification settings - Fork 237
Description
Problem
Returning Smithy-modeled errors from middleware requires a workaround. The code generator only implements IntoResponse<Protocol> for operation error enums, not for individual error shapes. Middleware is operation-agnostic and cannot use operation-specific error enums. This forces users to define dummy operations solely to generate error enums with IntoResponse, polluting the API model.
Proposed solution: Generate IntoResponse<Protocol> for error shapes defined at the service level, enabling middleware to use them directly.
Background
How smithy-rs generates error types
When you define an operation with errors in a Smithy model:
@error("client")
@httpError(404)
structure ResourceNotFound {
@required
message: String
}
operation GetPokemon {
input: GetPokemonInput
output: GetPokemonOutput
errors: [ResourceNotFound, ValidationException]
}The code generator produces:
- Individual error structures (e.g.,
ResourceNotFound,ValidationException) - An operation error enum that wraps all possible errors for that operation:
pub enum GetPokemonError {
ResourceNotFound(ResourceNotFound),
ValidationException(ValidationException),
}IntoResponse<Protocol>implementation for the operation error enum:
impl IntoResponse<RestJson1> for GetPokemonError {
fn into_response(self) -> Response<BoxBody> {
match serialize_get_pokemon_error(&self) {
Ok(response) => response,
Err(e) => { /* error handling */ }
}
}
}This IntoResponse implementation handles protocol-specific serialization (e.g., for RestJson1, it produces {"__type": "ResourceNotFound", "message": "..."}).
The middleware problem
Tower middleware (layers and plugins) often need to return errors before the request reaches the operation handler. For example, an authentication middleware might reject unauthorized requests.
The challenge is that middleware is operation-agnostic—it does not know which operation is being called. It cannot return GetPokemonError because the request might be for a different operation entirely.
Individual error structures like ResourceNotFound do not have IntoResponse implemented. Only the operation error enums do. This means middleware cannot directly use Smithy-modeled errors with proper protocol serialization.
Current workaround
Users define a dummy operation whose sole purpose is to generate an error enum with IntoResponse:
/// Dummy operation for middleware errors. Not a real API.
operation Middleware {
errors: [NotAuthorized, ValidationError, InternalError]
}This generates:
pub enum MiddlewareError {
NotAuthorized(NotAuthorized),
ValidationError(ValidationError),
InternalError(InternalError),
}
impl IntoResponse<RestJson1> for MiddlewareError { /* ... */ }Middleware can then use this enum:
impl<S> Service<Request<Body>> for AuthMiddleware<S> {
fn call(&mut self, req: Request<Body>) -> Self::Future {
if !is_authorized(&req) {
return Box::pin(async {
Ok(IntoResponse::<RestJson1>::into_response(
MiddlewareError::NotAuthorized(NotAuthorized { message: "...".into() })
))
});
}
// ...
}
}This approach works but pollutes the API model with a fake operation
Proposed solution
Generate IntoResponse<Protocol> for individual error shapes defined at the service level:
service MyService {
errors: [NotAuthorized, InternalError]
operations: [GetPokemon, ListPokemon]
}The code generator should produce IntoResponse<Protocol> for each service-level error:
impl IntoResponse<RestJson1> for NotAuthorized {
fn into_response(self) -> Response<BoxBody> {
match serialize_not_authorized_error(&self) {
Ok(response) => response,
Err(e) => { /* error handling */ }
}
}
}This enables middleware to use service-level errors directly:
Ok(IntoResponse::<RestJson1>::into_response(
NotAuthorized { message: "...".into() }
))Implementation notes
Current code generation structure
The code generator currently produces two levels of serialization:
- Individual error JSON serializer (e.g.,
shape_resource_not_found_exception.rs):
pub fn ser_resource_not_found_exception_error(
value: &crate::error::ResourceNotFoundException,
) -> Result<String, SerializationError> {
// Serializes the error struct to a JSON string
}- Operation error HTTP response builder (e.g.,
shape_get_pokemon_species.rs):
pub fn ser_get_pokemon_species_http_error(
error: &crate::error::GetPokemonSpeciesError,
) -> Result<Response, ResponseRejection> {
match error {
GetPokemonSpeciesError::ResourceNotFoundException(output) => {
let payload = ser_resource_not_found_exception_error(output)?;
Response::builder()
.status(404) // From @httpError
.header("x-amzn-errortype", "ResourceNotFoundException")
.header("content-type", "application/json")
.body(payload)?
}
// ... other variants
}
}The JSON body serialization is already factored out per error shape. However, the HTTP response construction (status code, headers) is embedded in each operation's error serializer function.
Required changes
To generate IntoResponse for individual error structs, the code generator needs to also factor out the HTTP response construction for each error shape:
// New: Per-error HTTP response builder
fn ser_resource_not_found_exception_http_response(
error: &ResourceNotFoundException,
) -> Result<Response, ResponseRejection> {
let payload = ser_resource_not_found_exception_error(error)?;
Response::builder()
.status(404)
.header("x-amzn-errortype", "ResourceNotFoundException")
.header("content-type", "application/json")
.body(payload)
}
// Individual error struct now has IntoResponse
impl IntoResponse<RestJson1> for ResourceNotFoundException {
fn into_response(self) -> Response<BoxBody> {
ser_resource_not_found_exception_http_response(&self)
.unwrap_or_else(|e| /* error fallback */)
}
}
// Operation error enum delegates to the same function
pub fn ser_get_pokemon_species_http_error(
error: &GetPokemonSpeciesError,
) -> Result<Response, ResponseRejection> {
match error {
GetPokemonSpeciesError::ResourceNotFoundException(inner) => {
ser_resource_not_found_exception_http_response(inner)
}
// ... other variants
}
}This ensures both code paths use the same serialization logic, avoiding duplication.
Benefits
- Eliminates the need for dummy operations
- Provides a clear, documented pattern for middleware error handling
- Aligns with the semantic purpose of service-level errors (errors common to all operations)