Skip to content

Commit

Permalink
Merge pull request #16 from xenit-eu/problem-details
Browse files Browse the repository at this point in the history
Add problem-details package
  • Loading branch information
vierbergenlars authored Sep 28, 2023
2 parents d9a4287 + 8a3384c commit 7fcb5aa
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 0 deletions.
24 changes: 24 additions & 0 deletions packages/problem-details/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# `@contentgrid/problem-details`

[RFC 9457](https://datatracker.ietf.org/doc/html/rfc9567) Problem Details types and helpers

## Usage

```typescript
import { ProblemDetail, ProblemDetailError, checkResponse } from '@contentgrid/problem-details'

// Fetch data from somewhere
fetch('/some-url')
.then(checkResponse) // Throws ProblemDetailError if an error response is returned
.then(response => {
// Handle succesfull response
}, error => {
// Handle error response
if(error instanceof ProblemDetailError) {
// ProblemDetail is available on the ProblemDetailError
console.error("Failed to fetch", error.problemDetail.title)
} else {
console.error("Failed to fetch", error);
}
})
```
91 changes: 91 additions & 0 deletions packages/problem-details/__tests__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { test, expect, describe } from "@jest/globals"
import { fromResponse } from "../src/index";

describe('Create problem details', () => {

test('from a problemdetails response', async () => {
const response = new Response(JSON.stringify({
type: "http://example.com/rels/xyz",
title: "XYZ"
}), {
status: 404,
headers: {
"Content-Type": "application/problem+json"
}
});

const problemDetails = await fromResponse(response);

expect(problemDetails).toEqual({
type: "http://example.com/rels/xyz",
status: 404,
title: "XYZ"
});
})

test('from a problemdetails response with an "about:blank" type', async () => {
const response = new Response(JSON.stringify({
type: "about:blank",
}), {
status: 404,
headers: {
"Content-Type": "application/problem+json"
}
});

const problemDetails = await fromResponse(response);

expect(problemDetails).toEqual({
status: 404,
title: ""
});
})

test('from a problemdetails response with a null type', async () => {
const response = new Response(JSON.stringify({
type: null,
}), {
status: 404,
headers: {
"Content-Type": "application/problem+json"
}
});

const problemDetails = await fromResponse(response);

expect(problemDetails).toEqual({
status: 404,
title: ""
});
})

test('from a non-problemdetails response', async () => {
const response = new Response("my-response", {
status: 403,
statusText: "Forbidden",
headers:{
"Content-Type": "text/plain"
}
});

const problemDetails = await fromResponse(response);

expect(problemDetails).toEqual({
status: 403,
title: "Forbidden"
});

})

test('from a succesful response', async () => {
const response = new Response("my data", {
status: 200,
headers: {
"Content-Type": "text/plain"
}
});

expect(await fromResponse(response)).toBeNull();

});
})
1 change: 1 addition & 0 deletions packages/problem-details/babel.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "../../config/babel.config.mjs";
1 change: 1 addition & 0 deletions packages/problem-details/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../config/jest.config.js');
36 changes: 36 additions & 0 deletions packages/problem-details/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@contentgrid/problem-details",
"version": "0.0.1-alpha.0",
"description": "RFC9547 Problem Details types and helpers",
"keywords": ["RFC7807", "RFC9547", "problem", "problem-details"],
"main": "./build/index.js",
"module": "./build/index.mjs",
"types": "./build/index.d.ts",
"exports": {
".": {
"types": "./build/index.d.ts",
"import": "./build/index.mjs",
"default": "./build/index.js"
}
},
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.22.6",
"tslib": "^2.6.1"
},
"scripts": {
"prepare": "rollup -c",
"test": "jest"
},
"repository": {
"type": "git",
"url": "https://github.com/xenit-eu/contentgrid-ts",
"directory": "packages/problem-details"
},
"publishConfig": {
"access": "public"
},
"files": [
"build"
]
}
1 change: 1 addition & 0 deletions packages/problem-details/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "../../config/rollup.config.mjs";
47 changes: 47 additions & 0 deletions packages/problem-details/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export interface ProblemDetail {
readonly type?: string;
readonly status: number;
readonly title: string;
readonly detail?: string;
readonly instance?: string;
}

export async function fromResponse<T extends ProblemDetail>(response: Response): Promise<T | null> {
if(response.ok) {
return null;
}
if(response.headers.get("content-type")?.toLowerCase() !== "application/problem+json") {
return {
status: response.status,
title: response.statusText
} as T;
}

const data = await response.json();

data.status ??= response.status;
data.title ??= response.statusText;
if(data.type === null || data.type === "about:blank") {
delete data.type;
}

return data as T;
}

export class ProblemDetailError<T extends ProblemDetail> extends Error {
constructor(public readonly problemDetail: T) {
super(problemDetail.title);
this.name = 'ProblemDetailError';
Object.setPrototypeOf(this, new.target.prototype);
}
}

export async function checkResponse(response: Response): Promise<Response> {
const problem = await fromResponse(response);

if(problem !== null) {
throw new ProblemDetailError(problem);
}

return response;
}
6 changes: 6 additions & 0 deletions packages/problem-details/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../../config/tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
}
}

0 comments on commit 7fcb5aa

Please sign in to comment.