Skip to content

Error Handling & Edge Cases

Robust error handling is essential for a reliable integration. This chapter covers the HTTP status codes you will encounter, the error response formats, and guidance for handling edge cases.

HTTP Status Code Reference

StatusMeaningTypical Causes
200SuccessRequest processed successfully
400Bad RequestInvalid state transition, malformed payload, missing required fields
401UnauthorizedMissing, invalid, or expired API key
404Not FoundResource (order, command) does not exist
409ConflictOptimistic locking conflict (concurrent modification)
429Too Many RequestsRate limit exceeded (not currently implemented, but plan for it)
500Internal Server ErrorUnexpected server error
503Service UnavailableTemporary server unavailability

Error Response Shapes

The API uses two different error response formats.

ProblemDetail (RFC 7807)

Most errors from the GlobalExceptionHandler use the standard ProblemDetail format:

json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Invalid order state transition",
  "instance": "/orders/12345/statusupdate"
}

The type field is typically "about:blank". The title is a short classification. The detail contains a human-readable explanation. The instance is the request URI that caused the error.

Common ProblemDetail responses:

ScenarioStatusTitleDetail
Invalid state transition400Illegal argumentNo enum constant ...
Missing required field400Illegal argumentLocation name is required.
Invalid order ID (order does not exist)400Illegal argumentOrder does not exist
Validation error400Validation error(validation message)
Database constraint violation400Constraint violationGeneric constraint message
License error401License error(license-related message)
Database operation error400Something went wrong during database operation.(error details)
Internal server error500Internal server error{uuid}: {message}

Each 500 response includes a UUID for tracing. Log this UUID when troubleshooting.

Legacy 401 Responses

Authentication errors return two possible shapes:

From AuthorizationInterceptor (missing/invalid key):

http
HTTP/1.1 401 Unauthorized
Content-Type: text/plain

{"error" : "Session not valid."}

Note: This response has Content-Type: text/plain, not application/json, even though the body is JSON. The response includes a random delay of 500-1499ms.

From UnAuthorizedException handler (expired session):

json
{
  "message": "Unauthorized",
  "uid": null
}

Both indicate the same thing: the API key is not valid. Re-authenticate or obtain a new key.

Edge Cases

SSE Reconnection with Offset Parameters

When the SSE connection drops:

  1. Detect the drop via onerror callback (EventSource) or connection timeout.
  2. Implement exponential backoff: 1s, 2s, 4s, 8s, capped at 60s.
  3. On reconnection, pass the last processed order number and command ID: /notifications/service/stream?orderNumber=12345&commandNumber=42
  4. If the server has restarted, your session may be lost. Obtain a new API key.
javascript
function reconnect(apiKey, lastOrderNumber, lastCommandNumber) {
  const url = `https://{host}/notifications/service/stream` +
    `?api_key=${apiKey}` +
    `&orderNumber=${lastOrderNumber}` +
    `&commandNumber=${lastCommandNumber}`;
  // ... open new EventSource
}

Network Partitioning

If the network partitions (the SSE stream drops but the client cannot reach the server), the SSE connection will time out after 60 minutes of inactivity. However, the keepalive events (every 15 seconds) will fail much sooner.

Recovery strategy:

  1. The SSE onerror fires quickly when keepalives stop arriving.
  2. Start polling GET /orders/location at a regular interval (every 5 seconds) as a fallback.
  3. Attempt SSE reconnection simultaneously.
  4. Once SSE is re-established, resume SSE-driven polling and reduce the fallback interval.

Duplicate Detection

The number field in each ExternalOrder is a unique, monotonically increasing identifier. Use it for idempotency:

javascript
const processedOrders = new Set();

function processOrder(order) {
  if (processedOrders.has(order.number)) {
    return; // Already processed
  }
  processedOrders.add(order.number);
  // ... process the order
}

Keep the set of processed IDs at least until the order is completed or aborted. You can clear old entries after a safe period (e.g., 1 hour).

Partial Data — Nullable Fields

Several fields in ExternalOrder can be null. Your client must handle these gracefully:

  • tableNumber — May be null for takeout orders. Treat as "no table assigned."
  • userNumber — May be null if the order was created without a user session.
  • state — May be null for command objects. Commands always have an implicit PLACED state.
  • rowsnull for command objects. Only orders have rows.
  • togo — May be null for dine-in orders.
  • moveTableTo — Non-null only for move-table commands.
  • readTableState — Non-null only for read-table-state commands.
  • paymentNumber — Non-null only for request-bill commands.
  • paymentDiscount — May be null if no discounts were applied.
  • customer — May be null if no customer information was provided.

Safe access pattern:

javascript
// Use optional chaining and nullish coalescing
const table = order.tableNumber ?? "unknown";
const state = order.state ?? "PLACED";
const rows = order.rows ?? [];
const paymentDiscounts = order.paymentDiscount ?? [];

Order State Machine

text
                          +---> COMPLETED
                          |
  SELECTING --> NEW --> PLACED ----> ABORTED
                          |
                          +---> FAILED ---> PLACED (retry)
  • SELECTING: Customer is adding items. Not visible to service clients via the location endpoint.
  • NEW: Order created by a client app. Not yet placed.
  • PLACED: Customer submitted the order. This is the state your service client sees when polling.
  • COMPLETED: Order fulfilled. Set by your service client via status update.
  • ABORTED: Order cancelled. Set by your client or when a session closes.
  • FAILED: Processing error. A server-side retry job may reset failed orders back to PLACED.

When you update an order's status, the server records timestamps:

  • orderTime — Set when the order transitions to PLACED
  • completeTime — Set when the order transitions to COMPLETED or ABORTED
  • failedTime — Set when the order transitions to FAILED

Concurrency — Only One Client Should Process Each Order

If multiple service clients are polling the same location, they will both receive the same orders. To avoid duplicate processing:

  1. Use the order number for deduplication (see above).
  2. The first client to call PUT /orders/{id}/statusupdate succeeds. The second client gets a 400 error because the state has changed.
  3. If the second client gets a 400, it should verify the order's current state via polling and skip it if already processed.
  4. For optimistic locking conflicts (409), the server retries internally. If the conflict persists, refresh your view of the order.

Handling Timeouts

  • SSE stream: 60-minute inactivity timeout. Keepalives prevent this in normal operation.
  • HTTP requests: Set a reasonable timeout of 30 seconds for regular API calls.
  • SSE connection: Set a timeout of at least 65 seconds to account for keepalive intervals.
  • Network failures: Retry with exponential backoff. Do not retry indefinitely.

Curl: Simulating Error Scenarios

Invalid API Key

bash
curl -i -H "api_key: invalid-key" "https://{host}/orders/location"

Non-existent Order

bash
curl -i -X PUT \
  -H "api_key: abc123-def456-ghi789" \
  -H "Content-Type: application/json" \
  -d '{"newState": "COMPLETED"}' \
  "https://{host}/orders/99999/statusupdate"

Invalid JSON Payload

bash
curl -i -X PUT \
  -H "api_key: abc123-def456-ghi789" \
  -H "Content-Type: application/json" \
  -d 'not-json' \
  "https://{host}/orders/12345/statusupdate"

Next Steps