Repository: unjs/h3 (ts)

Issue:
Allow for parsing of error status codes from non-http response errors.
### Describe the feature

Hey there 👋 

I'm an [Apollo Client](https://www.apollographql.com/docs/react/) maintainer. I'd like to start a discussion in regards to error handling when using Apollo Client with Nuxt/h3 and see if we can collaborate on a solution.

We recently had [this issue](https://github.com/apollographql/apollo-client/issues/11634) opened in our repo noting that network errors thrown by Apollo Client aren't forwarding the status code to h3's underlying `H3Error` due to how the status code is parsed in h3's [error creation](https://github.com/unjs/h3/blob/main/src/error.ts) functions. It appears `createError` expects a [`statusCode` property](https://github.com/unjs/h3/blob/27f6fad97dd0292d82fe30d9a7f4fca7511763ea/src/error.ts#L102) in order to set this on the `H3Error` instance. Unfortunately this doesn't integrate well with Apollo Client because Apollo Client wraps all errors in its own `ApolloError` instance. The status code is lost since the status code is instead found in `error.networkError.statusCode`.

I'd love to contribute a way to allow for these libraries to work better together. Unfortunately we are stuck with this error structure for the foreseeable future and we'd like to avoid adding a `statusCode` property to `ApolloError` itself, especially since Apollo Client's core aims to be framework agnostic. 

One potential solution is that we could add a [`cause`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) from `ApolloError` to the original error. With this perhaps `createError` could try to parse `status`/`statusCode` from `error.cause` if its available?

I'd love to get your thoughts on this! At the very least, perhaps a recommendation I could take back to the original issue would be useful. I'll be honest, I'm pretty unfamiliar with Nuxt/h3, so any help would be appreciated. Thanks!

### Additional information

- [X] Would you be willing to help implement this feature?

Pull request description:
feat(createError): inherit fields from `cause` as fallback
Resolves #691

Updates the logic for detecting the `statusCode` from an error by checking if it can be found on `error.cause`. This helps better detect a status code from an error created by a library that creates their own wrapped errors (such as [Apollo Client](https://www.apollographql.com/docs/react/)'s [`ApolloError`](https://github.com/apollographql/apollo-client/blob/7111de266e4ae5b21cf8cb59531db6f8bb9b443f/src/errors/index.ts#L74-L114) class).

See https://github.com/apollographql/apollo-client/issues/11634 for more context on this change. We've opened https://github.com/apollographql/apollo-client/pull/11902 on our end to make this integration possible.

Patch:
diff --git a/src/error.ts b/src/error.ts
index 956e3f9..784c25a 100644
--- a/src/error.ts
+++ b/src/error.ts
@@ -93,8 +93,11 @@ export function createError<DataT = unknown>(
     return input;
   }
 
+  // Inherit H3Error properties from cause as fallback
+  const cause: unknown = input.cause;
+
   const err = new H3Error<DataT>(input.message ?? input.statusMessage ?? "", {
-    cause: input.cause || input,
+    cause: cause || input,
   });
 
   if (hasProp(input, "stack")) {
@@ -117,25 +120,32 @@ export function createError<DataT = unknown>(
     err.data = input.data;
   }
 
-  if (input.statusCode) {
-    err.statusCode = sanitizeStatusCode(input.statusCode, err.statusCode);
-  } else if (input.status) {
-    err.statusCode = sanitizeStatusCode(input.status, err.statusCode);
-  }
-  if (input.statusMessage) {
-    err.statusMessage = input.statusMessage;
-  } else if (input.statusText) {
-    err.statusMessage = input.statusText as string;
+  const statusCode =
+    input.statusCode ??
+    input.status ??
+    (cause as H3Error)?.statusCode ??
+    (cause as { status?: number })?.status;
+  if (typeof statusCode === "number") {
+    err.statusCode = sanitizeStatusCode(statusCode);
   }
-  if (err.statusMessage) {
-    err.statusMessage = sanitizeStatusMessage(err.statusMessage);
+
+  const statusMessage =
+    input.statusMessage ??
+    input.statusText ??
+    (cause as H3Error)?.statusMessage ??
+    (cause as { statusText?: string })?.statusText;
+  if (statusMessage) {
+    err.statusMessage = sanitizeStatusMessage(statusMessage);
   }
 
-  if (input.fatal !== undefined) {
-    err.fatal = input.fatal;
+  const fatal = input.fatal ?? (cause as H3Error)?.fatal;
+  if (fatal !== undefined) {
+    err.fatal = fatal;
   }
-  if (input.unhandled !== undefined) {
-    err.unhandled = input.unhandled;
+
+  const unhandled = input.unhandled ?? (cause as H3Error)?.unhandled;
+  if (unhandled !== undefined) {
+    err.unhandled = unhandled;
   }
 
   return err;


Test patch:
diff --git a/test/error.test.ts b/test/error.test.ts
index 3b35c75..8b1cfaa 100644
--- a/test/error.test.ts
+++ b/test/error.test.ts
@@ -103,4 +103,24 @@ describe("error", () => {
 
     expect(ctx.errors[0].cause).toBeInstanceOf(CustomError);
   });
+
+  it("can inherit from cause", async () => {
+    class CustomError extends Error {
+      cause = createError({
+        statusCode: 400,
+        statusMessage: "Bad Request",
+        unhandled: true,
+        fatal: true,
+      });
+    }
+
+    ctx.app.use("/", () => {
+      throw createError(new CustomError());
+    });
+
+    const res = await ctx.request.get("/");
+    expect(res.status).toBe(400);
+    expect(ctx.errors[0].unhandled).toBe(true);
+    expect(ctx.errors[0].fatal).toBe(true);
+  });
 });