From 171f75f68f5efda3312ac049218e4e130ed6f270 Mon Sep 17 00:00:00 2001 From: Gabriel Sullice Date: Tue, 11 Mar 2025 13:48:56 +0100 Subject: [PATCH] convert: every error to a problem details object --- index.js | 25 ------------------------- src/client.js | 22 ++++++++++++++-------- src/client_test.js | 12 ++++++++---- src/errors.js | 37 ++++++++++++++++++++++++++++--------- 4 files changed, 50 insertions(+), 46 deletions(-) diff --git a/index.js b/index.js index 472014c..b399b16 100644 --- a/index.js +++ b/index.js @@ -1,28 +1,3 @@ import Client, { bootstrap } from "./src/client.js"; -import { - ImplementationError, - LibraryError, - MissingContentTypeError, - RequestError, - ResponseError, - ServerError, - UnexpectedContentError, - UnexpectedContentTypeError, - UnprocessableResponseError, - UsageError, -} from "./src/errors.js"; - export default Client; export { bootstrap }; -export { - ImplementationError, - LibraryError, - MissingContentTypeError, - RequestError, - ResponseError, - ServerError, - UnexpectedContentError, - UnexpectedContentTypeError, - UnprocessableResponseError, - UsageError, -}; diff --git a/src/client.js b/src/client.js index 510abc3..183ecc1 100644 --- a/src/client.js +++ b/src/client.js @@ -1,5 +1,6 @@ import { ImplementationError, + LibraryError, MissingContentTypeError, RequestError, ServerError, @@ -122,7 +123,7 @@ export default function Client(initialURL) { return; } lastResource = resource || lastResource; - lastProblem = problem; + lastProblem = problem instanceof LibraryError ? problem.detail : problem; lastURL = url || lastURL; send(); }; @@ -200,7 +201,6 @@ export default function Client(initialURL) { mimeType.startsWith("application/problem+json")) ) { const problem = new UnexpectedContentTypeError( - response, `the server responded in with an unrecognizable media type: ${mimeType}`, { response }, ); @@ -251,25 +251,31 @@ export default function Client(initialURL) { update(id, { resource, url }); return true; } - const errorDetails = (doc.errors || []).filter((e) => e.detail).map((e) => - `detail: ${e.detail}` + console.assert( + mimeType.startsWith("application/vnd.api+json") || + mimeType.startsWith("application/problem+json"), ); + const errorDetail = mimeType.startsWith("application/problem+json") + ? doc.detail + : (doc.errors || []).filter((e) => e.detail).map((e) => + `detail: ${e.detail}` + ).join(", "); if (response.status >= 400 && response.status <= 499) { const problem = new RequestError( [ "request error", `${response.status} ${response.statusText}`, - ...errorDetails, + errorDetail, ].join(": "), - { doc, response }, + { response }, ); update(id, { problem, url }); } else if (response.status >= 500 && response.status <= 599) { const problem = new ServerError(response, [ "response error", `${response.status} ${response.statusText}`, - ...errorDetails, - ], { doc, response }); + errorDetail, + ], { response }); update(id, { problem, url }); } else { throw new ImplementationError( diff --git a/src/client_test.js b/src/client_test.js index d07c660..3680659 100644 --- a/src/client_test.js +++ b/src/client_test.js @@ -2,10 +2,8 @@ import Client, { isLocalURL } from "./client.js"; import { assert, assertEquals, - assertInstanceOf, } from "https://deno.land/std@0.185.0/testing/asserts.ts"; import TestServer from "./internal/testing/server.js"; -import { UnexpectedContentTypeError } from "./errors.js"; Deno.test("Client", async (t) => { const serverOptions = { hostname: "0.0.0.0", port: 3003 }; @@ -135,7 +133,10 @@ Deno.test("Client", async (t) => { let { status } = client.response(); assertEquals(status, 200); assertEquals(resource, undefined); - assertInstanceOf(problem, UnexpectedContentTypeError); + assertEquals( + problem.type, + "https://docs.applura.com/client/v2/errors#UnexpectedContentTypeError", + ); // Get a good response. server.respondWith( new Response( @@ -163,7 +164,10 @@ Deno.test("Client", async (t) => { ({ status } = client.response()); assertEquals(status, 200); assertEquals(resource.id, "200 resource"); - assertInstanceOf(problem, UnexpectedContentTypeError); + assertEquals( + problem.type, + "https://docs.applura.com/client/v2/errors#UnexpectedContentTypeError", + ); client.stop(); }); }); diff --git a/src/errors.js b/src/errors.js index de7abd1..5ece9f3 100644 --- a/src/errors.js +++ b/src/errors.js @@ -4,11 +4,18 @@ export class LibraryError extends Error { super(message, options); this.name = "LibraryError"; } + get detail() { + return { + type: `https://docs.applura.com/client/v2/errors#${this.name}`, + title: this.name, + detail: this.message, + }; + } } // Raised when the implementation of this library has caused an error. For example, when a known edge case has not been // handled. -export class ImplementationError extends Error { +export class ImplementationError extends LibraryError { constructor(message, options) { super(message, options); this.name = "ImplementationError"; @@ -23,22 +30,34 @@ export class UsageError extends LibraryError { } } -// Raised when an HTTP request causes an HTTP client error, i.e. for HTTP status codes >=300 and <=399. -export class RequestError extends LibraryError { - constructor(message, { doc, response, ...options }) { +// Raised when an HTTP response is in error, i.e. for HTTP status codes >=400. +export class HTTPError extends LibraryError { + constructor(message, { response, ...options }) { super(message, options); this.name = "RequestError"; - Object.defineProperty(this, "doc", { value: doc }); Object.defineProperty(this, "response", { value: response }); } + get detail() { + return { + ...super.detail, + status: this.response.status, + }; + } +} + +// Raised when an HTTP request causes an HTTP client error, i.e. for HTTP status codes >=400 and <=499. +export class RequestError extends HTTPError { + constructor(message, options) { + super(message, options); + this.name = "RequestError"; + } } // Raised when an HTTP response causes an error. -export class ResponseError extends LibraryError { - constructor(message, { doc, response, ...options }) { - super(message, { ...options, doc: { value: doc } }); +export class ResponseError extends HTTPError { + constructor(message, options) { + super(message, options); this.name = "ResponseError"; - Object.defineProperty(this, "response", { value: response }); } }