Skip to content

Add a context option for passing arbitrary data to hooks #677

Open
@Chinoman10

Description

@Chinoman10

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions