http-problem-details icon indicating copy to clipboard operation
http-problem-details copied to clipboard

Parsing a Problem Document from JSON

Open sazzer opened this issue 4 years ago • 7 comments

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 avatar Dec 20 '20 10:12 sazzer

@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?

AlexZeitler avatar Dec 21 '20 12:12 AlexZeitler

@sazzer I think https://github.com/badgateway/ketting#introduction can do this for you.

AlexZeitler avatar Apr 11 '21 20:04 AlexZeitler

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 avatar Apr 12 '21 06:04 sazzer

@sazzer You can follow the progress for a parser: https://github.com/PDMLab/http-problem-details-parser

Looking forward to your feedback.

AlexZeitler avatar Jun 08 '21 00:06 AlexZeitler

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.

robincafolla avatar Jun 14 '21 15:06 robincafolla

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 avatar Jun 14 '21 16:06 robincafolla

@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.

AlexZeitler avatar Jun 16 '21 00:06 AlexZeitler