aws / aws-pdk

The AWS PDK provides building blocks for common patterns together with development tools to manage and build your projects.
https://aws.github.io/aws-pdk/
Apache License 2.0
367 stars 73 forks source link

[FEATURE] Return error message sent from backend instead of standard message in ResponseError #821

Open raghibomar786 opened 1 month ago

raghibomar786 commented 1 month ago

Describe the feature

In case of error, it is important to know the reason of failure. In our use case, we need to display the reason for failure to the user. The backend sends the reason for failure in the error message, but pdk internally returns standard static message : "Response returned an error code". Is there a way to get the actual error message that the backend returns? If not, we should update this to pass back the actual error message sent by the backend.

Code:

protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise<Response> {
        const { url, init } = await this.createFetchParams(context, initOverrides);
        const response = await this.fetchApi(url, init);
        if (response && (response.status >= 200 && response.status < 300)) {
            return response;
        }
        throw new ResponseError(response, 'Response returned an error code');
    }
export class ResponseError extends Error {
    override name: "ResponseError" = "ResponseError";
    constructor(public response: Response, msg?: string) {
        super(msg);
    }
}

Use Case

There's a UI screen that can fail for multiple reason. We need to tell the user the exact reason for the failure. We send the exact reason from backend but there's no way to get it in frontend with the current code.

Proposed Solution

response is already present in the code, just need to propagate the message in the final function.

Other Information

No response

Acknowledgements

PDK version used

0.23.38

What languages will this feature affect?

Typescript, Java

Environment details (OS name and version, etc.)

Linux

cogwirrel commented 2 days ago

Hi @raghibomar786,

Thanks for raising this! It would definitely be nicer if the generated typescript client returned the error message by default!

Sadly the way the generated typescript client is generated by OpenAPI generator, it doesn't extract the body of an error response. The long term fix for this is to completely own our own templates for the generated client, so we can adjust how errors are handled by default. I'm not sure when we'll be able to do this as it will take a bit of work.

The way I've worked around this for projects using PDK is to add some middleware to the client which throws the error earlier than when the generated client throws the generic ResponseError. You can pass the middleware when you instantiate the client. This might look something like this if you use the CloudScapeReactTsWebsite:

In packages/website/src/hooks/useTypeSafeApiClient.ts:

import useSigV4Client from "@aws-northstar/ui/components/CognitoAuth/hooks/useSigv4Client";
import { DefaultApi as MyApiApi, Configuration as MyApiApiConfiguration, BadRequestErrorResponseContent, InternalFailureErrorResponseContent, NotFoundErrorResponseContent, NotAuthorizedErrorResponseContent, Middleware, ResponseContext } from "myapi-typescript-react-query-hooks";
import { useContext, useMemo } from "react";
import { RuntimeConfigContext } from "../components/RuntimeContext";

// Interface for API errors
export interface ApiError {
  readonly status: number;
  readonly details:
    | BadRequestErrorResponseContent
    | InternalFailureErrorResponseContent
    | NotFoundErrorResponseContent
    | NotAuthorizedErrorResponseContent
}

export const isApiError = (e: unknown): e is ApiError =>
  !!(e && typeof e === "object" && "status" in e && "details" in e);

/**
 * Middleware for handling API errors
 */
const errorHandlingMiddleware: Middleware = {
  post: async ({ response }: ResponseContext) => {
    if (response && response.status >= 400 && response.status < 600) {
      let details;
      try {
        details = await response.json();
      } catch (e) {
        // Unable to parse response body, so continue with default error handling
        return response;
      }
      throw <ApiError>{
        status: response.status,
        details,
      };
    }
    return response;
  },
};

export const useMyApiApiClient = () => {
  const client = useSigV4Client();
  const runtimeContext = useContext(RuntimeConfigContext);

  return useMemo(() => {
    return runtimeContext?.typeSafeApis?.["MyApi"]
      ? new MyApiApi(
          new MyApiApiConfiguration({
            basePath: runtimeContext.typeSafeApis["MyApi"],
            fetchApi: client,
            // Add the middleware here
            middleware: [errorHandlingMiddleware],
          })
        )
      : undefined;
  }, [client, runtimeContext?.typeSafeApis?.["MyApi"]]);
};

It then becomes possible to access the actual error response from the hooks, eg:

import { Alert } from "@cloudscape-design/components";
import { ApiError, errorTitle, isApiError } from "../../hooks/useTypeSafeApiClient";
import { useSayHello } from "myapi-typescript-react-query-hooks"

export const ExampleComponent = () => {

  const hello = useSayHello({ name: "Jack" });

  return hello.error && isApiError(hello.error) ? (
    <Alert type="error" header="Error">
      {hello.error.details.message}
    </Alert>
  ) : ...;

};

Hope that helps!

I'll leave this open as I think we can improve things by providing this middleware or similar as part of the generated client, which isn't as heavy a lift as owning all the templates for the generated typescript fetch client.

Cheers, Jack