Skip to content

Conversation

Matodor
Copy link

@Matodor Matodor commented Sep 27, 2025

No description provided.

Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

Copy link

changeset-bot bot commented Sep 27, 2025

⚠️ No Changeset found

Latest commit: 9544d05

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link

vercel bot commented Sep 27, 2025

@Matodor is attempting to deploy a commit to the Hey API Team on Vercel.

A member of the Team first needs to authorize it.

@dosubot dosubot bot added size:XS This PR changes 0-9 lines, ignoring generated files. bug 🔥 Something isn't working client Client package related labels Sep 27, 2025
Copy link
Member

@mrlubos mrlubos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brolnickij why doesn't this use spread? Seems quite brittle to do it this way

@brolnickij
Copy link
Contributor

@mrlubos in resolveOptions we build opts with internal request settings like auth, body serializers, custom validators, SSE callbacks and etc., we need them for interceptors and our own logic, but they are not valid options for ofetch

const resolveOptions = async (options: RequestOptions) => {
const opts = {
..._config,
...options,
headers: mergeHeaders(_config.headers, options.headers),
serializedBody: undefined,
};
if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
}
if (opts.requestValidator) {
await opts.requestValidator(opts);
}
if (opts.body !== undefined && opts.bodySerializer) {
opts.serializedBody = opts.bodySerializer(opts.body);
}
// remove Content-Type if body is empty to avoid invalid requests
if (opts.body === undefined || opts.serializedBody === '') {
opts.headers.delete('Content-Type');
}
// if a raw body is provided (no serializer), adjust Content-Type only when it
// equals the default JSON value to better match the concrete body type
if (
opts.body !== undefined &&
opts.bodySerializer === null &&
(opts.headers.get('Content-Type') || '').toLowerCase() ===
'application/json'
) {
const b: unknown = opts.body;
if (typeof FormData !== 'undefined' && b instanceof FormData) {
// let the runtime set the multipart boundary
opts.headers.delete('Content-Type');
} else if (
typeof URLSearchParams !== 'undefined' &&
b instanceof URLSearchParams
) {
// standard urlencoded content type (+ charset)
opts.headers.set(
'Content-Type',
'application/x-www-form-urlencoded;charset=UTF-8',
);
} else if (typeof Blob !== 'undefined' && b instanceof Blob) {
const t = b.type?.trim();
if (t) {
opts.headers.set('Content-Type', t);
} else {
// unknown blob type: avoid sending a misleading JSON header
opts.headers.delete('Content-Type');
}
}
}
// precompute network body (stability for retries and interceptors)
const networkBody = getValidRequestBody(opts) as
| RequestInit['body']
| null
| undefined;
const url = buildUrl(opts);
return { networkBody, opts, url };
};

buildOfetchOptions sends to $ofetch.raw only the keys that ofetch actually supports and normalizes them at the same time (clears query, overrides retry, forces ignoreResponseError and etc.)

export const buildOfetchOptions = (
opts: ResolvedRequestOptions,
body: BodyInit | null | undefined,
responseType: OfetchResponseType | undefined,
retryOverride?: OfetchOptions['retry'],
): OfetchOptions =>
({
agent: opts.agent as OfetchOptions['agent'],
body,
dispatcher: opts.dispatcher as OfetchOptions['dispatcher'],
headers: opts.headers as Headers,
ignoreResponseError:
(opts.ignoreResponseError as OfetchOptions['ignoreResponseError']) ??
true,
method: opts.method,
onRequest: opts.onRequest as OfetchOptions['onRequest'],
onRequestError: opts.onRequestError as OfetchOptions['onRequestError'],
onResponse: opts.onResponse as OfetchOptions['onResponse'],
onResponseError: opts.onResponseError as OfetchOptions['onResponseError'],
parseResponse: opts.parseResponse as OfetchOptions['parseResponse'],
// URL already includes query
query: undefined,
responseType,
retry: retryOverride ?? (opts.retry as OfetchOptions['retry']),
retryDelay: opts.retryDelay as OfetchOptions['retryDelay'],
retryStatusCodes:
opts.retryStatusCodes as OfetchOptions['retryStatusCodes'],
signal: opts.signal,
timeout: opts.timeout as number | undefined,
}) as OfetchOptions;

if we return return { ...opts, body }, all internal fields from resolveOptions go out too, and that can cause all kinds of issues

for example, when ofetch updates, any of those internal keys can become a public ofetch option with a different type, and then we end up sending to ofetch.raw something it does not expect (like a function instead of a boolean in requestValidator), which would give a runtime error

so in my view this filter / layer is needed, because it makes sure only ofetch-friendly data goes out (plus the normalization I mentioned above)

what do you think..?

@mrlubos
Copy link
Member

mrlubos commented Sep 27, 2025

@brolnickij No issue with that, but afraid more fields might be missing. Do you know if this is the only one that was missed? Does the ofetch API ever change? That could also cause issues, but I'm less worried about that

@brolnickij
Copy link
Contributor

@mrlubos

No issue with that, but afraid more fields might be missing. Do you know if this is the only one that was missed?

i went through FetchOptions / ResolvedFetchOptions in ofetch again, and among the standard RequestInit flags the only one we had actually missed was credentials

Does the ofetch API ever change? That could also cause issues, but I'm less worried about that

ofetch is pretty stable, but no one knows what the next release will bring..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug 🔥 Something isn't working client Client package related size:XS This PR changes 0-9 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants