// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { MetadataBearer } from '@aws-sdk/types'; import { HttpResponse, parseJsonError as parseAwsJsonError, } from '@aws-amplify/core/internals/aws-client-utils'; import { ApiErrorResponse } from '@aws-amplify/core/internals/utils'; import { RestApiError } from '../errors'; /** * Parses both AWS and non-AWS error responses coming from the users' backend code. * * AWS errors generated by the AWS services(e.g. API Gateway, Bedrock). They can be Signature errors, * ClockSkew errors, etc. These responses will be parsed to errors with proper name and message from the AWS * services. * * non-AWS errors thrown by the user code. They can contain any headers or body. Users need to access the * error.response to get the headers and body and parse them accordingly. The JS error name and message will * be `UnknownError` and `Unknown error` respectively. */ export const parseRestApiServiceError = async ( response?: HttpResponse, ): Promise<(RestApiError & MetadataBearer) | undefined> => { if (!response) { // Response is not considered an error. return; } const parsedAwsError = await parseAwsJsonError(stubErrorResponse(response)); if (!parsedAwsError) { // Response is not considered an error. } else { const bodyText = await response.body?.text(); return buildRestApiError(parsedAwsError, { statusCode: response.statusCode, headers: response.headers, body: bodyText, }); } }; /** * The response object needs to be stub here because the parseAwsJsonError assumes the response body to be valid JSON. * Although this is true for AWS services, it is not true for responses from user's code. Once the response body is * unwrapped as JSON(and fail), it cannot be read as text again. Therefore, we need to stub the response body here to * make sure we can read the error response body as a JSON, and may fall back to read as text if it is not a valid JSON. */ const stubErrorResponse = (response: HttpResponse): HttpResponse => { let bodyTextPromise: Promise | undefined; const bodyProxy = new Proxy(response.body, { get(target, prop, receiver) { if (prop === 'json') { // For potential AWS errors, error parser will try to parse the body as JSON first. return async () => { if (!bodyTextPromise) { bodyTextPromise = target.text(); } try { return JSON.parse(await bodyTextPromise!); } catch (error) { // If response body is not a valid JSON, we stub it to be an empty object and eventually parsed // as an unknown error return {}; } }; } else if (prop === 'text') { // For non-AWS errors, users can access the body as a string as a fallback. return async () => { if (!bodyTextPromise) { bodyTextPromise = target.text(); } return bodyTextPromise; }; } else { return Reflect.get(target, prop, receiver); } }, }); const responseProxy = new Proxy(response, { get(target, prop, receiver) { if (prop === 'body') { return bodyProxy; } else { return Reflect.get(target, prop, receiver); } }, }); return responseProxy; }; /** * Utility to create a new RestApiError from a service error. */ const buildRestApiError = ( error: Error & MetadataBearer, response?: ApiErrorResponse, ): RestApiError & MetadataBearer => { const restApiError = new RestApiError({ name: error?.name, message: error.message, underlyingError: error, response, }); // $metadata is only required for backwards compatibility. return Object.assign(restApiError, { $metadata: error.$metadata }); };