Skip to content

Commit

Permalink
Add handleErrorResponse to the public API
Browse files Browse the repository at this point in the history
This prevents from having to manually map the received error to an Error
class.
  • Loading branch information
NSeydoux committed Jul 3, 2024
1 parent 7f81d73 commit 33c14d6
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 16 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
`MethodNotAllowedError`, `NotAcceptableError`, `NotFoundError`, `PreconditionFailedError`,
`TooManyRequestsError`, `UnauthorizedError`, `UnsupportedMediaTypeError`: Specializations
of the `ClientHttpError` to represent common HTTP error responses.
- `handleErrorResponse`: a function to map the received HTTP error to the appropriate error
class.
77 changes: 77 additions & 0 deletions src/http/handleErrorResponse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// Copyright Inrupt Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { describe, it, expect } from "@jest/globals";
import { handleErrorResponse } from "./handleErrorResponse";
import {
BadRequestError,
BAD_REQUEST_STATUS,
} from "./wellKnown/badRequestError";
import { mockResponse } from "./httpError.mock";
import ConflictError, { CONFLICT_STATUS } from "./wellKnown/conflictError";
import ForbiddenError, { FORBIDDEN_STATUS } from "./wellKnown/forbiddenError";
import GoneError, { GONE_STATUS } from "./wellKnown/goneError";
import InternalServerErrorError, {
INTERNAL_SERVER_ERROR_STATUS,
} from "./wellKnown/internalServerErrorError";
import MethodNotAllowedError, {
METHOD_NOT_ALLOWED_STATUS,
} from "./wellKnown/methodNotAllowedError";
import NotAcceptableError, {
NOT_ACCEPTABLE_STATUS,
} from "./wellKnown/notAcceptableError";
import NotFoundError, { NOT_FOUND_STATUS } from "./wellKnown/notFoundError";
import PreconditionFailedError, {
PRECONDITION_FAILED_STATUS,
} from "./wellKnown/preconditionFailedError";
import TooManyRequestsError, {
TOO_MANY_REQUESTS_STATUS,
} from "./wellKnown/tooManyRequestsError";
import UnauthorizedError, {
UNAUTHORIZED_STATUS,
} from "./wellKnown/unauthorizedError";
import UnsupportedMediaTypeError, {
UNSUPPORTED_MEDIA_TYPE_STATUS,
} from "./wellKnown/unsupportedMediaTypeError";

describe("handleErrorResponse", () => {
it.each([
[BAD_REQUEST_STATUS, BadRequestError],
[CONFLICT_STATUS, ConflictError],
[FORBIDDEN_STATUS, ForbiddenError],
[GONE_STATUS, GoneError],
[INTERNAL_SERVER_ERROR_STATUS, InternalServerErrorError],
[METHOD_NOT_ALLOWED_STATUS, MethodNotAllowedError],
[NOT_ACCEPTABLE_STATUS, NotAcceptableError],
[NOT_FOUND_STATUS, NotFoundError],
[PRECONDITION_FAILED_STATUS, PreconditionFailedError],
[TOO_MANY_REQUESTS_STATUS, TooManyRequestsError],
[UNAUTHORIZED_STATUS, UnauthorizedError],
[UNSUPPORTED_MEDIA_TYPE_STATUS, UnsupportedMediaTypeError],
])("maps %i status to %s class", (responseStatus, errorClass) => {
const response = mockResponse({ status: responseStatus });
const error = handleErrorResponse(
response,
"Some response body",
"Some error message",
);
expect(error).toBeInstanceOf(errorClass);
});
});
122 changes: 122 additions & 0 deletions src/http/handleErrorResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//
// Copyright Inrupt Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import { ClientHttpError } from "./httpError";
import BadRequestError, {
BAD_REQUEST_STATUS,
} from "./wellKnown/badRequestError";
import ConflictError, { CONFLICT_STATUS } from "./wellKnown/conflictError";
import ForbiddenError, { FORBIDDEN_STATUS } from "./wellKnown/forbiddenError";
import GoneError, { GONE_STATUS } from "./wellKnown/goneError";
import InternalServerErrorError, {
INTERNAL_SERVER_ERROR_STATUS,
} from "./wellKnown/internalServerErrorError";
import MethodNotAllowedError, {
METHOD_NOT_ALLOWED_STATUS,
} from "./wellKnown/methodNotAllowedError";
import NotAcceptableError, {
NOT_ACCEPTABLE_STATUS,
} from "./wellKnown/notAcceptableError";
import NotFoundError, { NOT_FOUND_STATUS } from "./wellKnown/notFoundError";
import PreconditionFailedError, {
PRECONDITION_FAILED_STATUS,
} from "./wellKnown/preconditionFailedError";
import TooManyRequestsError, {
TOO_MANY_REQUESTS_STATUS,
} from "./wellKnown/tooManyRequestsError";
import UnauthorizedError, {
UNAUTHORIZED_STATUS,
} from "./wellKnown/unauthorizedError";
import UnsupportedMediaTypeError, {
UNSUPPORTED_MEDIA_TYPE_STATUS,
} from "./wellKnown/unsupportedMediaTypeError";

/**
* Map an HTTP error response to one of the Error classes exported by this library.
*
* @example
* ```ts
* const response = await fetch("https://example.org/resource");
* if (!response.ok) {
* const responseBody = await response.text();
* throw handleErrorResponse(response, responseBody, "Fetch got error response");
* }
* ```
*
* @param responseMetadata the response metadata
* @param responseBody the response body
* @param message the error message
* @returns an instance of the ClientHttpError subclass matching the response metadata status.
* If the response status is unkown, the generic ClientHttpError class is used.
* @since unreleased
*/
export function handleErrorResponse(
responseMetadata: {
status: number;
statusText: string;
headers: Headers;
url: string;
},
responseBody: string,
message: string,
): ClientHttpError {
switch (responseMetadata.status) {
case BAD_REQUEST_STATUS:
return new BadRequestError(responseMetadata, responseBody, message);
case CONFLICT_STATUS:
return new ConflictError(responseMetadata, responseBody, message);
case FORBIDDEN_STATUS:
return new ForbiddenError(responseMetadata, responseBody, message);
case GONE_STATUS:
return new GoneError(responseMetadata, responseBody, message);
case INTERNAL_SERVER_ERROR_STATUS:
return new InternalServerErrorError(
responseMetadata,
responseBody,
message,
);
case METHOD_NOT_ALLOWED_STATUS:
return new MethodNotAllowedError(responseMetadata, responseBody, message);
case NOT_ACCEPTABLE_STATUS:
return new NotAcceptableError(responseMetadata, responseBody, message);
case NOT_FOUND_STATUS:
return new NotFoundError(responseMetadata, responseBody, message);
case PRECONDITION_FAILED_STATUS:
return new PreconditionFailedError(
responseMetadata,
responseBody,
message,
);
case TOO_MANY_REQUESTS_STATUS:
return new TooManyRequestsError(responseMetadata, responseBody, message);
case UNAUTHORIZED_STATUS:
return new UnauthorizedError(responseMetadata, responseBody, message);
case UNSUPPORTED_MEDIA_TYPE_STATUS:
return new UnsupportedMediaTypeError(
responseMetadata,
responseBody,
message,
);
default:
return new ClientHttpError(responseMetadata, responseBody, message);
}
}

export default handleErrorResponse;
48 changes: 48 additions & 0 deletions src/http/httpError.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// Copyright Inrupt Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
// Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

import { PROBLEM_DETAILS_MIME } from "./problemDetails";

export const mockResponse = ({
body,
status,
statusText,
headers,
}: {
body?: string;
status?: number;
statusText?: string;
headers?: Headers;
responseUrl?: string;
} = {}): Response => {
const response = new Response(body ?? undefined, {
status: status ?? 400,
statusText: statusText ?? "Bad Request",
headers:
headers ??
new Headers({
"Content-Type": PROBLEM_DETAILS_MIME,
}),
});
return response;
};

export default mockResponse;
17 changes: 3 additions & 14 deletions src/http/httpError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,10 @@
import { describe, it, expect, jest } from "@jest/globals";
import { ClientHttpError } from "./httpError";
import { mockProblemDetails } from "./problemDetails.mock";
import {
DEFAULT_TYPE,
PROBLEM_DETAILS_MIME,
hasProblemDetails,
} from "./problemDetails";
import { DEFAULT_TYPE, hasProblemDetails } from "./problemDetails";
import InruptClientError from "../clientError";
import { hasErrorResponse } from "./errorResponse";
import mockResponseBase from "./httpError.mock";

const mockResponse = ({
body,
Expand All @@ -42,15 +39,7 @@ const mockResponse = ({
headers?: Headers;
responseUrl?: string;
} = {}): Response => {
const response = new Response(body ?? undefined, {
status: status ?? 400,
statusText: statusText ?? "Bad Request",
headers:
headers ??
new Headers({
"Content-Type": PROBLEM_DETAILS_MIME,
}),
});
const response = mockResponseBase({ body, status, statusText, headers });
jest
.spyOn(response, "url", "get")
.mockReturnValue(responseUrl ?? "https://example.org/resource");
Expand Down
2 changes: 1 addition & 1 deletion src/http/wellKnown/internalServerErrorError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { InruptClientError } from "../../clientError";
import type { ErrorResponse } from "../errorResponse";
import { ClientHttpError } from "../httpError";

export const INTERNAL_SERVER_ERROR_STATUS = 410 as const;
export const INTERNAL_SERVER_ERROR_STATUS = 500 as const;

export type InternalServerErrorErrorResponse = ErrorResponse & {
status: typeof INTERNAL_SERVER_ERROR_STATUS;
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
//

export { InruptClientError } from "./clientError";
export { ClientHttpError } from "./http/httpError";
export { ClientHttpError, handleErrorResponse } from "./http/httpError";
export {
DEFAULT_TYPE,
PROBLEM_DETAILS_MIME,
Expand Down

0 comments on commit 33c14d6

Please sign in to comment.