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

feat: experimental customize fetch option #94

Closed
wants to merge 3 commits into from

Conversation

panva
Copy link
Owner

@panva panva commented Jan 7, 2024

Refs #20
Refs #62
Refs #79
Refs #80

This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
compatible changes or removal may occur in any future release.

When configured on an interface that extends HttpRequestOptions, that's every options
parameter for functions that trigger HTTP Requests, this replaces the use of global fetch.
As a fetch replacement the arguments and expected return are the same as fetch.

In theory any module that claims to be compatible with the Fetch API can be used but your mileage
may vary. No workarounds to allow use of non-conform Responses will be considered.

If you only need to update the Request properties you do not need to use a Fetch API
module, just change what you need and pass it to globalThis.fetch just like this module
would normally do.

Its intended use cases are to enable

  • Request/Response tracing and logging
  • Custom caching strategies for responses of Authorization Server Metadata and JSON Web Key Set
    (JWKS) endpoints
  • Changing the Request properties like headers, body, credentials, mode before it is sent

Known issues:

  • Expect Type-related issues when passing the inputs through to fetch-like modules, they
    hardly ever get their typings inline with actual fetch, you should @ts-expect-error
    them.
  • Returning self-constructed Response instances prohibits AS-signalled DPoP Nonce caching.

Example

Using sindresorhus/ky hooks feature for logging outgoing
requests and their responses.

import ky from 'ky'
import { experimentalCustomFetch } from 'oauth4webapi'

// example use
await discoveryRequest(new URL('https://as.example.com'), {
  [experimentalCustomFetch]: (...args) =>
    ky(args[0], {
      ...args[1],
      hooks: {
        beforeRequest: [
          (request) => {
            logRequest(request)
          },
        ],
        beforeRetry: [
          ({ request, error, retryCount }) => {
            logRetry(request, error, retryCount)
          },
        ],
        afterResponse: [
          (request, _, response) => {
            logResponse(request, response)
          },
        ],
      },
    }),
})

Example

Using nodejs/undici for mocking.

import * as undici from 'undici'
import { discoveryRequest, experimentalCustomFetch } from 'oauth4webapi'

const mockAgent = new undici.MockAgent()
mockAgent.disableNetConnect()
undici.setGlobalDispatcher(mockAgent)

// continue as per undici documentation
// https://github.com/nodejs/undici/blob/v6.2.1/docs/api/MockAgent.md#example---basic-mocked-request

// example use
await discoveryRequest(new URL('https://as.example.com'), {
  // @ts-expect-error
  [experimentalCustomFetch]: undici.fetch,
})

@panva panva mentioned this pull request Jan 7, 2024
2 tasks
@panva
Copy link
Owner Author

panva commented Jan 7, 2024

The type of feedback i'm looking for here is

How would you use it, what do you mean to achieve and how would your implementation ~roughly look.

@panva
Copy link
Owner Author

panva commented Jan 7, 2024

@panva panva marked this pull request as ready for review January 7, 2024 17:32
test/userinfo.test.ts Dismissed Show dismissed Hide dismissed
@melbourne2991
Copy link

Thanks for this.

This looks good to me.

Below is an example of how I might use this feature:

import { discoveryRequest, experimentalCustomFetch } from 'oauth4webapi'

const traceInput = (input: RequestInfo | URL) => {
  // ...
}

const traceResponse = (response: Response) => {
  // ...
}

const shouldCache = (response: Response): boolean => {
  // ...
}

const cachedResponseStillValid = (response: Response): boolean => {
  // Not needed for Cloudflare as it's Cache API implementation respects Cache-Control headers
  // but may be needed for other environments (browser/deno)
}

const discoveryRequestCache = await caches.open("discovery-request-cache");

const customFetch: typeof fetch = async (input, init) => {
  const req = new Request(input, init);

  traceInput(req);

  const cachedResponse = await discoveryRequestCache.match(req);

  if (cachedResponse) {
    const stillValid = cachedResponseStillValid(cachedResponse);

    if (stillValid) {
      traceResponse(cachedResponse);
      return cachedResponse;
    } else {
      discoveryRequestCache.delete(req);
    }
  }

  const response = await fetch(req);

  traceResponse(response);

  if (shouldCache(response)) {
    discoveryRequestCache.put(req, response)
      .catch(err => console.log('Caching failed', err))
  }

  return response;
}

await discoveryRequest(new URL('https://as.example.com'), {
  // @ts-expect-error
  [experimentalCustomFetch]: customFetch,
})

panva added a commit that referenced this pull request Jan 9, 2024
panva added a commit that referenced this pull request Jan 9, 2024
panva added a commit that referenced this pull request Jan 9, 2024
panva added a commit that referenced this pull request Jan 9, 2024
panva added a commit that referenced this pull request Jan 10, 2024
@panva
Copy link
Owner Author

panva commented Jan 10, 2024

Released in v2.5.0, use #98 for feedback.

@panva panva closed this Jan 10, 2024
@panva panva deleted the experimental-custom-fetch branch January 11, 2024 11:22
@github-actions github-actions bot locked and limited conversation to collaborators Apr 15, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants