1

I'm working on an Aurelia CLI project, using TypeScript, and aurelia-fetch-client for making HTTP calls to a .NET Core / Web API backend.

The server standardizes unhandled exceptions by returning an ApiError JSON result as the body of API responses, with a status code in the 5xx range.

On the client, I want to use an interceptor that performs client-side publishing of these errors, but adding the interceptor is changing the control flow in an undesirable way.

This the relevant config in main.ts:

function configureContainer(container: Container) {
    const http = new HttpClient();
    http.configure((config: HttpClientConfiguration) => {
        config.useStandardConfiguration()
            .withBaseUrl("/api/")
            .withInterceptor(new ApiErrorInterceptor(container.get(EventAggregator)));
    });

    container.registerInstance(HttpClient, http);
}

And this is the interceptor:

import { autoinject } from "aurelia-framework";
import { EventAggregator } from "aurelia-event-aggregator";
import { Interceptor } from "aurelia-fetch-client";

@autoinject
export class ApiErrorInterceptor implements Interceptor {
    constructor(private readonly _aggregator: EventAggregator) {
    }

    responseError(error: any) {
        // do something with the error            

        return error;
    }
}

If the custom interceptor is NOT added, then a non-OK response is logged as an error in Chrome like this:

Unhandled rejection (<{}>, no stack trace)

But if the interceptor is added in, then promises no longer result in an unhandled rejection error, and the calling code continues as if the promise was resolved, which is NOT what we want, because it introduces a massive change to the control flow of the whole app.

How do I implement the responseError() handler so that the control flow works the same as before? Is this even possible, or am I misunderstanding the purpose of this handler?

I tried re-throwing the error insteading of returning, but this won't compile against the Interceptor interface. Should I be doing this in response() instead, for example?

EDIT:

This is what we ended up using, after looking at the Fetch source and related Aurelia sources in more detail. It handles server-based errors purely in the response() handler, and client-based connection errors in responseError() only.

import { autoinject } from "aurelia-framework";
import { EventAggregator } from "aurelia-event-aggregator";
import { Interceptor } from "aurelia-fetch-client";
import { TypedJSON } from "typedjson-npm";
import { EventNames } from "../model/EventNames";
import { ApiError } from "../model/ApiError";

@autoinject
export class ApiErrorInterceptor implements Interceptor {
    constructor(private readonly _aggregator: EventAggregator) {
    }

    async response(response: Response, request?: Request) {
        if (!response.ok) {
            // publish an error for the response
            const apiError = await this.translateToApiError(response.clone());
            if (apiError) {
                this._aggregator.publish(EventNames.apiError, apiError);
            }
        }

        return response;
    }

    responseError(error: any) {
        // publish errors resulting from connection failure, etc
        if (error instanceof Error) {
            const apiError = new ApiError();
            apiError.statusCode = 0;
            apiError.isError = true;
            apiError.message = error.message;

            this._aggregator.publish(EventNames.apiError, apiError);
        }

        return error;
    }

    private async translateToApiError(response: Response): Promise<ApiError> {
        let apiError: ApiError | undefined = undefined;
        const text = await response.text();

        if (text) {
            apiError = TypedJSON.parse(text, ApiError);
        }

        if (!apiError) {
            apiError = this.getHttpError(response);
        }

        return apiError;
    }

    private getHttpError(response: Response): ApiError {
        const apiError = new ApiError();
        apiError.isError = true;
        apiError.statusCode = response.status;
        apiError.message = "Unknown HTTP Error";

        switch (apiError.statusCode) {
            case 400:
                apiError.message = "Bad Request";
                break;

            case 401:
                apiError.message = "Unauthorized Access";
                break;

            case 404:
                apiError.message = "Not Found";
                break;
        }

        if (apiError.statusCode >= 500 && apiError.statusCode < 600) {
            apiError.message = "Internal Server Error";
        }

        return apiError;
    }
}
Sam
  • 6,167
  • 30
  • 39
  • Hi, did you take a look at the fetch source code? As far as I see, the fetch, as the http-client do never reject the promise. It will always call the response and you have to check the status code. I think that the way you are doing (returning the error) is the correct one, you just have to handle the errors in the response. – Bruno Marotta Jul 31 '17 at 07:57

2 Answers2

1

Using return Promise.reject(error); in place of return error; is compiling and working as expected using your code. Returning the error assumes you resolved the response and passes your return value to the resolved code path which is why you were seeing the undesired behavior.

Thorb
  • 81
  • 6
0

The error parameter of responseError is any, so make sure it is what you think it is. In my case, I was expecting the failed response, but got back a caught TypeError exception. This is my interceptor:

responseError(response: any): Promise<Response> {
    if (response instanceof Response) {
        return response.json().then((serverError: ServerError) => {

            // Do something with the error here.

            return Promise.reject<Response>(serverError.error);
        }); 
    }
}

I also have had issues stemming from the way my exception handling middleware is configured in the .NET Core app. In my case, the UseDeveloperExceptionPage was causing CORS error in the fetch client. It doesn't sound like this is the same problem as yours, but you can compare your configuration to mine here.

Jonathan Eckman
  • 2,071
  • 3
  • 23
  • 49