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;
}
}