Skip to content

HATEOAS Error Handling — Hypermedia Error Responses with Remediation Links

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about HATEOAS Error Handling. We cover key concepts, practical examples, and best practices to help you master this topic.

HATEOAS error handling extends standard error responses with hypermedia controls, including links to remediation actions, documentation, and retry endpoints that guide clients toward resolution.

What You'll Learn

  • RFC 7807 Problem Details for REST errors
  • Adding remediation links to errors
  • Guiding clients to fix errors
  • Validation errors with field-level links
  • Rate Limiting with retry links
  • Error documentation via describedBy

Why It Matters

Standard error responses tell you something is wrong but not how to fix it. Hypermedia error responses include links to documentation, retry endpoints, and remediation actions. DodaTech's Durga Antivirus Pro returns errors with links to help clients automatically handle rate limits, validation failures, and authorization issues.

Real-World Use

A client tries to quarantine a device but doesn't have permission. The error response includes a link to the authentication endpoint, a link to request elevated permissions, and a link to the API documentation about authorization. The client can show these options to the user.

flowchart TB
    A["Client Action"] --> B["Server Error"]
    B --> C["Problem Details JSON"]
    C --> D["type: /errors/insufficient-permissions"]
    C --> E["title: Permission Denied"]
    C --> F["detail: User lacks admin role"]
    C --> G["_links: auth, request-access, docs"]
    D --> H["Client shows remediation options"]
    G --> H
    H --> I["Follow /request-access"]
    H --> J["Follow /auth to re-login"]
    style B fill:#fecaca,stroke:#dc2626
    style C fill:#fef3c7,stroke:#d97706

Code Examples

{
  "type": "https://errors.dodatech.com/insufficient-permissions",
  "title": "Insufficient Permissions",
  "status": 403,
  "detail": "User 'john.doe@example.com' does not have the 'admin' role required to quarantine devices.",
  "instance": "/devices/1/quarantine",
  "timestamp": "2026-06-28T10:30:00Z",
  "correlationId": "req-abc-123",
  "_links": {
    "describedBy": {
      "href": "https://docs.dodatech.com/errors/insufficient-permissions",
      "title": "Error Documentation"
    },
    "auth": {
      "href": "/auth/login",
      "title": "Re-authenticate"
    },
    "request-access": {
      "href": "/access-requests/new?resource=/devices/1/quarantine",
      "title": "Request Admin Access"
    },
    "help": {
      "href": "/support/tickets/new?error=insufficient-permissions",
      "title": "Contact Support"
    }
  }
}
const express = require('express');
const app = express();

class HypermediaError extends Error {
  constructor(type, title, status, detail, instance, links = {}) {
    super(detail);
    this.type = type;
    this.title = title;
    this.status = status;
    this.detail = detail;
    this.instance = instance;
    this.links = links;
  }
}

function problemResponse(err, req, res) {
  const body = {
    type: err.type || 'https://errors.dodatech.com/generic-error',
    title: err.title || 'Error',
    status: err.status || 500,
    detail: err.detail || err.message,
    instance: err.instance || req.originalUrl,
    timestamp: new Date().toISOString(),
    correlationId: req.correlationId,
  };
  
  if (err.links && Object.keys(err.links).length > 0) {
    body._links = err.links;
  }
  
  return res.status(err.status || 500).json(body);
}

// Validation error with field-level remediation
function validationError(errors, instance) {
  const fieldLinks = {};
  for (const field of Object.keys(errors)) {
    fieldLinks[`fix:${field}`] = {
      href: `/docs/fields/${field}`,
      title: `How to fix '${field}'`,
    };
  }
  
  return new HypermediaError(
    'https://errors.dodatech.com/validation-error',
    'Validation Failed',
    422,
    errors.map(e => `${e.field}: ${e.message}`).join('; '),
    instance,
    {
      describedBy: { href: 'https://docs.dodatech.com/errors/validation', title: 'Validation Docs' },
      ...fieldLinks,
    },
  );
}

// Rate limit error with retry link
function rateLimitError(retryAfter, instance) {
  return new HypermediaError(
    'https://errors.dodatech.com/rate-limited',
    'Rate Limit Exceeded',
    429,
    `Too many requests. Retry after ${retryAfter} seconds.`,
    instance,
    {
      retryAfter: { href: instance, title: `Retry after ${retryAfter}s` },
      upgrade: { href: '/pricing', title: 'Upgrade for higher limits' },
      describedBy: { href: 'https://docs.dodatech.com/errors/rate-limiting', title: 'Rate Limits' },
    },
  );
}

// Error handler middleware
app.use((err, req, res, next) => {
  if (err instanceof HypermediaError) {
    return problemResponse(err, req, res);
  }
  
  // Generic error
  return problemResponse(
    new HypermediaError(
      'https://errors.dodatech.com/internal-error',
      'Internal Server Error',
      500,
      'An unexpected error occurred',
      req.originalUrl,
      {
        describedBy: { href: 'https://docs.dodatech.com/errors/internal', title: 'Error Info' },
      },
    ),
    req, res,
  );
});

Example 3: Client Handling Hypermedia Errors

class HypermediaErrorClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }
  
  async request(url, options = {}) {
    const res = await fetch(`${this.baseUrl}${url}`, options);
    
    if (!res.ok) {
      const error = await res.json();
      await this.handleError(error);
    }
    
    return res.json();
  }
  
  async handleError(error) {
    console.error(`Error: ${error.title} (${error.status})`);
    console.error(`Details: ${error.detail}`);
    console.error(`Correlation ID: ${error.correlationId}`);
    
    const links = error._links || {};
    
    // Follow remediation links based on error type
    if (error.type?.includes('validation-error')) {
      console.log('Validation errors found. Fix fields:');
      for (const [rel, link] of Object.entries(links)) {
        if (rel.startsWith('fix:')) {
          const field = rel.replace('fix:', '');
          console.log(`  - Fix '${field}': ${link.href}`);
        }
      }
    }
    
    if (error.type?.includes('rate-limited')) {
      if (links.retryAfter) {
        // Parse Retry-After header or use link
        const retrySeconds = this.parseRetryAfter(error);
        console.log(`Rate limited. Retrying in ${retrySeconds}s...`);
        
        await this.delay(retrySeconds * 1000);
        // The client doesn't need to know the URL - it's in the link
        return this.request(error.instance);
      }
    }
    
    if (error.type?.includes('insufficient-permissions')) {
      if (links['request-access']) {
        console.log(`Request access: ${links['request-access'].href}`);
        // Automatically navigate to access request
        const requestResult = await this.fetch(links['request-access'].href);
        console.log('Access request submitted:', requestResult);
      }
    }
    
    if (error.type?.includes('not-found')) {
      if (links.collection) {
        console.log(`Resource not found. Browse collection: ${links.collection.href}`);
      }
    }
    
    throw new Error(error.detail);
  }
}

Common Mistakes

  1. Returning plain error without links — if you return a 403 with just "Forbidden", the client has no idea what to do. Add links for auth, access requests, and docs.
  2. Using absolute URLs for everything — use relative URLs for same-API links and absolute URLs only for external documentation links.
  3. Not including a correlation ID — without a correlation ID, clients can't reference errors in support tickets. Always include one.
  4. Ignoring the type field — the type URI is the machine-readable error identifier. Each error type should have a dereferenceable documentation page.
  5. Not adding rate limit headers with links — return both Retry-After header and a retry link. Some clients read headers, others read body.

Practice Questions

  1. What is RFC 7807 Problem Details and how does it structure errors?
  2. How do remediation links help clients handle errors?
  3. What links should a validation error include?
  4. How does a rate-limited client use hypermedia links?
  5. When should you use relative vs absolute URLs in error links?

Challenge: Design a hypermedia error response for each HTTP status code (400, 401, 403, 404, 409, 422, 429, 500) with appropriate remediation links for a threat management API.

Mini Project

Build a hypermedia error handling system with: RFC 7807 Problem Details format, remediation links per error type (auth, validation, rate limit, not found, conflict), automatic link generation based on error context, client library for automated error recovery, and error documentation pages served from the API.

FAQ

Do I need HATEOAS in error responses?

Yes. Errors are the most important place for hypermedia because clients need guidance on how to recover. Without remediation links, the client can't programmatically fix errors.

What is the 'type' field in RFC 7807?

The type field is a URI that identifies the error type. It should point to a human-readable documentation page explaining the error and how to fix it.

How do I handle validation errors with multiple fields?

Return a 422 with a list of field errors. Include a fix:fieldName link for each invalid field pointing to documentation or a form to correct it.

Should error links include authentication tokens?

No. Never include auth tokens in error responses. Auth links should point to login endpoints where the client can obtain tokens.

How do I test hypermedia error handling?

Use integration tests that assert error responses include expected links. For each error condition, verify the correct remediation links are present.

What's Next

Learn HATEOAS API design

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro