Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

openapi-fetch: add discriminant property to FetchResponse #2071

Closed
OliverJAsh opened this issue Dec 26, 2024 · 1 comment
Closed

openapi-fetch: add discriminant property to FetchResponse #2071

OliverJAsh opened this issue Dec 26, 2024 · 1 comment
Labels
enhancement New feature or request openapi-fetch Relevant to the openapi-fetch library

Comments

@OliverJAsh
Copy link

OliverJAsh commented Dec 26, 2024

Consider the following code:

import createFetchClient from "openapi-fetch";
import type { paths } from "./schema";

const fetchClient = createFetchClient<paths>({
  baseUrl: "https://httpbin.org",
});

const run = async () => {
  const result = await fetchClient.GET("/status/{code}", {
    params: { path: { code: 500 } },
  });
  const { data, error } = result;

  // ❌ This will be evaluated as `false`.
  if (error) {
    console.log("error", error);
  } else {
    // ❌
    // TypeScript has narrowed `data` to a non-nullable type.
    // But, at runtime, `data` is `undefined`.
    console.log("data", data);
  }
};

run();

In this example, openapi-fetch will return an error of value undefined, because content length is 0 and the response is not okay:

// handle empty content
if (response.status === 204 || response.headers.get("Content-Length") === "0") {
return response.ok ? { data: undefined, response } : { error: undefined, response };
}

When this happens, the if (error) branch above will be evaluated as false, despite the fact there was actually an error (albeit undefined). (This is also a problem in openapi-react-query.)

TypeScript will then narrow the type of data to a non-nullable type (i.e. exclude undefined). But at runtime, data is actually undefined.

We can mitigate this by avoiding destructuring and using the in operator like so:

const run = async () => {
  const result = await fetchClient.GET("/status/{code}", {
    params: { path: { code: 500 } },
  });

  // ✅ This will be evaluated as `true`.
  if ("error" in result) {
    // ✅
    console.log("error", result.error);
  } else {
    console.log("data", result.data);
  }
};

However, it's a shame to lose destructuring. For this reason I wanted to suggest that openapi-fetch could return a discriminant property in FetchResponse, e.g.:

 export type FetchResponse<T extends Record<string | number, any>, Options, Media extends MediaType> =
   | {
+      responseOk: true;
       data: ParseAsResponse<SuccessResponse<ResponseObjectMap<T>, Media>, Options>;
       error?: never;
       response: Response;
     }
     | {
+      responseOk: false;
       data?: never;
       error: ErrorResponse<ResponseObjectMap<T>, Media>;
       response: Response;
     };

Then, destructuring would be safe again:

const run = async () => {
  const result = await fetchClient.GET("/status/{code}", {
    params: { path: { code: 500 } },
  });

  const { data, error, responseOk } = result;

  // ✅ This will be evaluated as `true`.
  if (!responseOk) {
    // ✅
    console.log("error", error);
  } else {
    console.log("data", data);
  }
};
@OliverJAsh OliverJAsh added bug Something isn't working openapi-fetch Relevant to the openapi-fetch library labels Dec 26, 2024
@OliverJAsh OliverJAsh changed the title openapi-fetch: FetchResponse should return discriminant property openapi-fetch: add discriminant property to FetchResponse Dec 26, 2024
@drwpow drwpow added enhancement New feature or request and removed bug Something isn't working labels Jan 3, 2025
@drwpow
Copy link
Contributor

drwpow commented Jan 3, 2025

Just wanted to add that this technically isn’t a bug; this is intentional behavior as it relates to empty responses (see that PR, and attached issues, for discussion on the topic).

In general, the following is best practice:

const { data, error } = result;
if (error) {
  // show error
} else {
  // 
}

Because it’s highly-unusual to not serve any response whatsoever on server error. It is more acceptable to return an empty response for 2xx codes, but even when POSTing or PATCHing it is generally better to return something.

This meshes philosophically with using OpenAPI in the first place—if you aren’t sending responses to the client you’re missing out on a huge benefit of using OpenAPI in the first place (sure, your requests are still typed, but responses are the lions share of what people use OpenAPI for).

I appreciate the suggestion but at this time we won’t consider adding this, because it:

  • only applies to empty responses
  • only applies to situations when BOTH your data and error responses are expected to be empty

Designing an API around this edge case isn’t ideal, because you, knowing your schema, could work around this, and/or update your schema and endpoints to return more responses in scenarios where it would make sense to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request openapi-fetch Relevant to the openapi-fetch library
Projects
Development

No branches or pull requests

2 participants