Skip to content

ServiceError should return :unprocessable_entity not :bad_request by default #1

@obie

Description

@obie

Problem

The base ServiceError.api_error method returns :bad_request (400) as the default error code:

# lib/servus/support/errors.rb:26
class ServiceError < StandardError
  def api_error
    { code: :bad_request, message: message }
  end
end

This means whenever services call failure(message), they return a 400 status code. However, these are typically validation or business logic errors, not malformed request errors.

Expected Behavior

  • 400 Bad Request: Malformed requests, syntax errors, invalid JSON
  • 422 Unprocessable Entity: Semantic validation errors - request is well-formed but business logic rejects it

Examples from ZAR Core's Digital Cash V2:

  • "Digital cash must be in 'deposited' status" → should be 422, not 400
  • "Only the original depositor can reclaim this digital cash" → should be 422, not 400
  • "Escrow amount mismatch" → should be 422, not 400

Current Impact

ZAR Core has to work around this with a mapping layer in ApiErrorHelpers:

# Temporary workaround - maps :bad_request to :unprocessable_entity
status = error_params.last == 'bad_request' ? :unprocessable_entity : error_params.last.to_sym

This hides the problem and means every app using Servus has to implement their own workaround.

Proposed Solution

Change the default in ServiceError.api_error:

class ServiceError < StandardError
  def api_error
    { code: :unprocessable_entity, message: message }
  end
end

BadRequestError would remain unchanged (still returns 400) for actual bad request scenarios.

Migration Impact

This is a breaking change - any services currently relying on failure() returning 400 will start returning 422. However:

  1. The new behavior is more semantically correct
  2. It matches Rails/HTTP conventions better
  3. ZAR Core already expects 422 for these errors

Alternative

If backward compatibility is critical, we could:

  1. Add a new ValidationError that returns 422
  2. Keep ServiceError at 400
  3. Update documentation to recommend using specific error types instead of failure()

But I think fixing the default is better since Servus is purpose-built for ZAR.


Related: ZAR Core PR #814 (Digital Cash V2) exposed this issue when adopting run_service pattern.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions