Description
I'd like to propose a new context
or ctx
option (for greater, "anything goes" extensibility), which would solve two problems for me (one of which I've found a not-very-elegant workaround, but the other I haven't been able to yet), and potentially other users too.
Use-case 1:
Env Variables in Serverless environments
In typical serverless environments, such as Cloudflare Workers, environment variables (specially secrets) are passed along in a 'context', which is often "ping-ponged" between functions so each of the business logic functions can use it for whatever they need.
Because of this, I cannot statically (on build) define a KyInstance with an api-key
header, since I won't have it when the instance is created.
My chosen workaround (I've had a few) was wrapping the ky.extends in a function that receives the api-key, and injects it. Something like this:
const api = (c: Context<{ Bindings: Bindings }>) =>
ky.extend({
prefixUrl: 'https://domain.com/v99',
headers: {
'api-key': c.env.API_KEY,
},
});
const smtp = (c: Context) =>
api(c).extend((api) => ({
prefixUrl: `${api.prefixUrl}/smtp/email`,
}));
const contacts = (c: Context) =>
api(c).extend((api) => ({
prefixUrl: `${api.prefixUrl}/contacts`,
}));
Which then I have to replace all my smtp.post('', { json: body }).json()
with smtp(c).post(...).json()
.
If I could pass along something like: smtp.post('', { json: body, ctx: c }).json()
, I would then be able to use a beforeRequest
hook to inject the API key in the headers.
Custom Errors
Before I found out Ky existed, I used to do some code like this:
// utils.ts
const postJsonHeaderData = () => ({
method: "POST",
headers: {
Accept: "application/json",
"content-type": "application/json",
},
});
export const requestJson = (bodyData: any) => ({
...structuredClone(postJsonHeaderData()),
body: JSON.stringify(bodyData),
});
export const handleResponse = (r: Response, num?: number, errorDescription?: string) => {
if (!r.ok)
throw fetchError(r, num, errorDescription);
const contentType = r.headers.get("Content-Type");
if (contentType && contentType.includes("application/json"))
return r.json();
else
return r.text();
}
const fetchError = (r: Response, num?: number, description?: string) => (
new Error(
`Error (${r.status}) with fetch #${num ?? 0}${description? ` (${description})`: ''} - '${
r.statusText
}' from URL: ${r.url}`
)
);
export const genericError = (r: Response, num?: number, description?: string) => {
return new Error(
`Error (${r.status}) with request #${num ?? 0}${description? ` (${description})`: ''} - '${
r.statusText
}' from URL: ${r.url}`
);
}
Which would be used as such:
// some-email-logic.ts
export async function createContact(env: Env, user: { email: string, attributes: { FIRSTNAME: string, LASTNAME: string }}):
Promise<BrevoContact>
{
const method = HttpMethods.POST;
return fetch(`${BREVO_API_CONTACTS_ENDPOINT}`,
requestJson(env.BREVO_API_KEY, method, user)
).then((r) => handleResponse(r, 0, `Creating ${user.email}'s contact on Brevo`));
}
Which gives me really good logging --> when an error occurs, the function that made the request already passed what it was trying to do in a 'human text', and I can use a request number (in this case 0) if I'm retrying multiple times.
I'm not sure how to do this with Ky, since I can't extend Ky Instances with custom functions or properties for me to use in the Hooks (onError
, in this case).
If I had a ctx: Object
(which I could extend to my heart's content), I could do something like this:
return await contacts(c)
.post('', {
json: user,
ctx: {
'task-description': `Creating ${user.email}'s contact on Brevo`)),
},
})
.json<BrevoContact>();
Which would then be picked up a custom beforeError
hook.