http-problem-details
http-problem-details copied to clipboard
Parsing a Problem Document from JSON
This package works great for the server-side, but it would be great if it were usable on the client-side as well by being able to parse an incoming JSON document into a ProblemDocument
, complete with access to any extension values in a safe manner.
Cheers
@sazzer Thanks for using the package.
Could you please explain what and how you would expect to use it?
Would you be willing to send a PR?
@sazzer I think https://github.com/badgateway/ketting#introduction can do this for you.
It can - I improved the TypeScript support for that exact code myself. However, Ketting is designed for working with Hypermedia APIs, and it's a bit wasteful to use it for APIs that are not hypermedia in nature. Often if you want to work with such an API, you're better off just using something like Axios (or Fetch, from the browser).
Cheers
On Sun, 11 Apr 2021 at 21:15, Alexander Zeitler @.***> wrote:
@sazzer https://github.com/sazzer I think https://github.com/badgateway/ketting#introduction can do this for you.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/PDMLab/http-problem-details/issues/20#issuecomment-817365963, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAQEGGOBHBI6TYQKZIMBJTTIH7MVANCNFSM4VC73FHA .
@sazzer You can follow the progress for a parser: https://github.com/PDMLab/http-problem-details-parser
Looking forward to your feedback.
I'm using this library on the frontend in a couple projects. This is how I'm handling them (I'm not using it on the backend, which is PHP in my case)
class Api {
get(url, queryParams = []) {
let urlWithParams = url + params(queryParams);
return this.handleProblem(fetch(urlWithParams, {
credentials: 'same-origin',
method: 'GET',
mode: 'same-origin',
cache: 'no-cache',
}));
}
/**
* @param {Promise} result
* @return {Promise}
*/
handleProblem = (result) => {
return new Promise(((resolve, reject) => {
result.then((response) => {
// If the data returned is a problem
if (response.headers.get("content-type") === "application/problem+json" ) {
const clonedResponse = response.clone();
response.json()
.then((json) => {
// If we didn't request the problem's actual URL
if (json.type && response.url !== json.type) {
const extensions = json.extensions || null;
const problem = new ProblemDocument(json, extensions);
reject(problem);
} else {
resolve(clonedResponse);
}
})
.catch((error) => {
log('json error', error);
reject(error)
});
} else {
resolve(response);
}
}).catch((error) => {
log('unknown fetch error', error);
reject(error);
});
}));
};
}
Obviously that code is snipped from a larger project, but it might help someone else.
I should add that to get this library working on the front-end, if you're using webpack >= v5, you need to shim the require('url')
call here.
Webpack removed it's nodejs polyfills in v5 so you need to install the native-url
library and specify a resolve in your webpack.config.js
:
module.exports = {
// ...
resolve: {
fallback: {
"url": require.resolve("native-url")
}
}
};
If this project is planning true front-end support (and it would be great to have it) it would make sense to use the WHATWG url api instead.
@robincafolla thanks for showing your use case.
Right now I'm experimenting with parsing and trying to understand what is required.
So given this HTTP 400 problem detail repsonse,
{
"type": "https://example.net/validation-error",
"status": 400,
"title": "Your request parameters didn't validate.",
"instance": "https://example.net/account/logs/123",
"invalid-params": [
{
"name": "age",
"reason": "must be a positive integer"
},
{
"name": "color",
"reason": "must be 'green', 'red' or 'blue'"
}
]
}
you could do this:
const problemDocument = fromJSON(status400JSON)
This would give you the typed representation of the problem according to RFC7807 but without the extension invalid-params
.
As the type is not about:blank
but a specific problem document type https://example.net/validation-error
, the API docs for this particular problem can provide the schema for the extension.
On the client side we could then map the extension invalid-params
as well.
I'm thinking of something like this (it's in the PR linked in my previous comment):
const mappers: HttpProblemExtensionMapper[] = [
{
type: 'https://example.net/validation-error',
map: (object: any) =>
new ProblemDocumentExtension({
'invalid-params': object['invalid-params']
})
}
]
const problemDocument = fromJSON(status400JSON, mappers)
This would give us the document as shown above but including the extension.
So the idea is to have mappers for known extension schemas.
I'm also thinking about if it could make sense to have types for the specific problem documents like this:
type ValidationProblemDocument = ProblemDocument & {
type: 'https://example.net/validation-error'
'invalid-params': {
name: string
reason: string
}[]
}
No we could have type guards to make evaluating the documents a bit more type safe in our code:
function isValidationProblemDocument(
value: unknown
): value is ValidationProblemDocument {
const x = value as ProblemDocument
return x.type === 'https://example.net/validation-error'
}
if (isValidationProblemDocument(document)) {
document['invalid-params'].length.should.equal(2)
}
That way we can even get IntelliSense for invalid-params
within the if
statement.
But I'm also thinking about if it's a good idea to go that far. I wonder if it wouldn't be better to have our own error types living in the client and one could just map the HTTP problems to our client errors.