© The Author(s), under exclusive license to APress Media, LLC , part of Springer Nature 2021
D. TangPro Ember Datahttps://doi.org/10.1007/978-1-4842-6561-1_9

9. Handling Custom Error Responses

David Tang1  
(1)
Playa Vista, CA, USA
 

So far we’ve looked at working with APIs that return success responses. In this chapter, we will look at how Ember Data handles errors out of the box and the different adapter and serializer methods we can override to handle custom error responses. Let’s dive in!

Validation Errors

Responses with an HTTP status code of 422 (Unprocessable Entity) are considered invalid errors, or in other words, validation errors. Validation error responses follow the JSON:API specification regardless of which serializer is used. A JSON:API error response looks like the following:
{
  "errors": [
    {
      "id": "{unique identifier for this particular occurrence}",
      "links": {
        "about": "{link that leads to further details about this problem}"
      },
      "status": "{HTTP status code}",
      "code": "{application-specific error code}",
      "title": "{summary of the problem}",
      "detail": "{explanation specific to this occurrence of the problem}",
      "source": {
        "pointer": "{a JSON Pointer to the associated entity in the request document}",
        "parameter": "{a string indicating which URI query parameter caused the error}"
      },
      "meta": {}
    }
  ]
}

The response must contain a root key errors that is an array of error objects. Each error object can have any of the properties listed earlier. To find out more about each property of an error object, visit the JSON:API error documentation.1 JSON:API states that an error object may have those properties, but Ember Data only requires a subset of them.

Let’s say we want to create a new contact record and handle the scenario when there is an error validating the name attribute. The error response needs two properties: detail and source :
{
  "errors": [
    {
      "detail": "Name must be at least 2 characters.",
      "source": {
        "pointer": "data/attributes/name"
      }
    }
  ]
}

The value of source.pointer in an error object is a JSON Pointer2 to a specific attribute, which in this case is the name attribute. A JSON Pointer is a string using a syntax that is similar to a file path, where it identifies a path to a specific value in a JSON document.

When the adapter sees a 422 response status code, a rejected promise is returned with an instance of InvalidError to signal that the record failed server-side validation. The InvalidError instance is passed errors from the response payload. These validation errors can then be retrieved from the record with the errors property. For example, we can render the record’s validation errors for the name attribute as follows:
{{#each model.errors.name as |error|}}
  <div class="error">
    {{error.message}}
  </div>
{{/each}}
We can also access these errors and the instance of InvalidError in our catch block:
try {
  await contact.save();
} catch (invalidError) {
  console.log(invalidError); // instance of InvalidError
  console.log(contact.errors); // instance of Errors
  console.log(contact.get('errors.name')); // array of error objects for the name attribute
  console.log(contact.isValid); // false
}

Controlling the Invalid Status Code

Your first question might be “What if my API returns a status code other than 422?” If we look at the RESTAdapter source code, we can see the creation of InvalidError only happens if the status is invalid via the isInvalid() method :
isInvalid(status, headers, payload) {
  return status === 422;
},
handleResponse(status, headers, payload, requestData) {
  if (this.isSuccess(status, headers, payload)) {
    return payload;
  } else if (this.isInvalid(status, headers, payload)) {
    return new InvalidError(payload.errors);
  }
  // ...
  return new AdapterError(errors, detailedMessage);
}
We can override the public isInvalid() method in our adapter to account for a status code that isn’t 422. For example, maybe our API responds with 400 instead of 422:
app/adapters/application.js
import RESTAdapter from '@ember-data/adapter/rest';
export default class ApplicationAdapter extends RESTAdapter {
  namespace = 'api';
  isInvalid(status) {
    return status === 400;
  }
}

Controlling Error Response Payloads

Your second question might be “What if my error response payload doesn’t follow JSON:API?”

Let’s say our error response looks like the following instead:
{
  "errors": {
    "name": "Name must be at least 2 characters."
  }
}

If the payload contains an errors property that isn’t an array, you will get the following error message in the console:

Error

AdapterError expects json-api formatted errors array.

The reason this error message mentions AdapterError instead of InvalidError is because InvalidError extends from the AdapterError class.

Let’s look at the implementation of handleResponse() again:
handleResponse(status, headers, payload, requestData) {
  if (this.isSuccess(status, headers, payload)) {
    return payload;
  } else if (this.isInvalid(status, headers, payload)) {
    return  new InvalidError(payload.errors);
  }
  // ...
  return new AdapterError(errors, detailedMessage);
}
As you can see, the errors property of the payload is passed directly to InvalidError, and if it isn’t an array, the preceding error message will be thrown. To handle a custom error payload that either doesn’t have errors as an array or doesn’t have an errors property at all, we can override handleResponse() in the adapter:
app/adapters/application.js
import RESTAdapter from 'ember-data/adapters/rest';
export default class ApplicationAdapter extends RESTAdapter {
  namespace = 'api';
  handleResponse(status, headers, payload, requestData) {
    if (this.isInvalid(status)) {
      payload.errors = Object.keys(payload.errors).map((attribute) => {
        return {
          detail: payload.errors[attribute],
          source: {
            pointer: 'data/attributes/${attribute}'
          }
        };
      });
    }
    return super.handleResponse(status, headers, payload, requestData);
  }
}

In the preceding code, we are manipulating our custom error payload to be JSON:API compliant if the status is invalid. Then, the original handleResponse() is called so that the InvalidError object is created.

Now your third question might be “What if the payload does have an errors property that is an array, but it isn’t JSON:API compliant?” Let’s say our error payload looked like this instead:
{
  "errors": [
    {
      "attribute": "name",
      "messages": {
        "size": "Name must be at least 2 characters.",
        "alpha": "Name must be entirely alphabetic characters."
      }
    }
  ]
}
Each of the validation rules and corresponding messages are stored under errors/messages. To handle this error payload, we can override handleResponse() as before, or we can override the extractErrors() method on the serializer. The extractErrors() method is used to extract model errors when a call to save() on the model fails with an InvalidError. The extractErrors() method receives the payload as one of its arguments and expects the return value to look like the following:
{
  name: [
    'Name must be at least 2 characters.',
    'Name must be entirely alphabetic characters.'
  ];
}
Here is an implementation of overriding extractErrors() to normalize the errors array in the response:
app/serializers/contact.js
import ApplicationSerializer from './application';
export default class ContactSerializer extends ApplicationSerializer {
  extractErrors(store, typeClass, payload, id) {
    let extractedErrors = {};
    payload.errors.forEach((error) => {
      extractedErrors[error.attribute] = Object.keys(error.messages).map(
        (rule) => {
          return error.messages[rule];
        }
      );
    });
    return extractedErrors;
  }
}

So your fourth question might be “Why bother using extractErrors() if I could just use handleResponse() for all cases?” The answer to that goes back to the question “what class is responsible for formatting request and response data?” That’s the serializer. Generally, if I can handle normalizing error responses in the serializer, I prefer that. If normalization isn’t possible in the serializer, then I will handle it in the adapter.

Other Error Types

We learned about the InvalidError class that gets instantiated when there is a 422 response. Ember Data also supports a few other error types for common HTTP status codes (Table 9-1).
Table 9-1

Common HTTP Status Codes

Error Class

HTTP Status Code

InvalidError

422

UnauthorizedError

401

ForbiddenError

403

NotFoundError

404

ConflictError

409

ServerError

500

An UnauthorizedError will get thrown if the HTTP status code is 401, which indicates that authorization is required and has either failed or not been provided. A ForbiddenError will get thrown if the HTTP status code is 403, which signals that the authenticated user doesn't have the necessary permissions for the request. A NotFoundError will get thrown if the HTTP status code is 404, which indicates that the server can't find a resource. A ConflictError will get thrown if the HTTP status code is 409, which indicates that there is a request conflict with the state of the server. For example, a 409 status could be returned if a user was trying to update a resource with stale data. Lastly, a ServerError will get thrown if the HTTP status code is 500, which indicates that there was an error on the server.

All of these error classes also extend from the AdapterError class.

Let’s say our API returns a 500 status code. When this happens, the following will hold true:
import AdapterError, {
  ServerError,
  TimeoutError
} from '@ember-data/adapter/error';
// ...
try {
  await contact.save();
} catch (serverError) {
  console.log(serverError instanceof AdapterError); // true
  console.log(serverError instanceof ServerError); // true
  console.log(serverError instanceof TimeoutError); // false
}

As we learned with InvalidError , the expected error payload needs to have an errors property as an array. This is true for any subclass of AdapterError .

Let’s say our API responds with a 500 status code and the following payload:
{
  "errors": [
    {
      "status": "500",
      "title": "There was an error on the server.",
      "detail": "Oh snap! Something went wrong.",
      "foo": "bar"
    },
    {
      "bar": "baz"
    }
  ]
}
With this payload, the errors property on our ServerError instance will be this:
[
  {
    status: '500',
    title: 'There was an error on the server.',
    detail: 'Oh snap! Something went wrong.',
    foo: 'bar'
  },
  {
    bar: 'baz'
  }
];
If an errors key isn’t present in the response, the adapter will create the following for errors on our ServerError instance:
[
  {
    status: '500',
    title: 'The backend responded with an error', // this message is created by Ember Data
    detail: '[object Object]' // A string representation of the payload
  }
];
If the API returns a string as a payload, such as “FAILED”, detail will be that string:
[
  {
    status: '500',
    title: 'The backend responded with an error', // this message is created by Ember Data
    detail: 'FAILED'
  }
];
Personally, I haven’t found the need to normalize error payloads that weren’t validation errors (InvalidError), but if you need to, you can override handleResponse() in the adapter like we did earlier. For example, let’s say the API returned a 500 with the following response:
{
  "status": "500",
  "title": "There was an error on the server.",
  "detail": "Oh snap! Something went wrong.",
  "foo": "bar"
}
In this case, errors on our ServerError instance would contain the following:
[
  {
    status: '500',
    title: 'The backend responded with an error', // this message is created by Ember Data
    detail: '[object Object]' // A string representation of the payload
  }
];
We could normalize this error response in handleResponse() as follows:
import RESTAdapter from '@ember-data/adapter/rest';
export default class ApplicationAdapter extends RESTAdapter {
  handleResponse(status, headers, payload, requestData) {
    if (status === 500) {
      let normalizedPayload = {
        errors: [payload]
      };
      return super.handleResponse(
        status,
        headers,
        normalizedPayload,
        requestData
      );
    }
    return super.handleResponse(status, headers, payload, requestData);
  }
}
Here we are taking the raw payload and putting it in an errors array in an object. With this change, errors on our ServerError instance would contain the following:
[
  {
    status: '500',
    title: 'There was an error on the server.',
    detail: 'Oh snap! Something went wrong.',
    foo: 'bar'
  }
];

Summary

Handling errors is an important aspect when working with APIs, and Ember Data is flexible enough to handle any error response. In this chapter, we looked at the default error handling in Ember Data and learned about validation errors and other adapter errors. Next up, we’ll learn about different ways of testing our Ember Data customizations.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.212.145