Skip to content

Conversation

@ytoshiki
Copy link
Contributor

@ytoshiki ytoshiki commented Jul 1, 2025

Overview

Support for custom predicate functions in both HTTP and GraphQL request handlers

http.get(({ request }) => request.url.includes('foo'), resolver)
http.post(async ({ request }) => {
  const body = await request.clone().json()
  return body.property === 'value'
}, resolver)
graphql.query(
  ({ operationName, variables }) =>
    operationName === 'GetUserDetail' && variables.id === 'abc-123',
  () => {
    return HttpResponse.json({
      data: {
        user: {
          firstName: 'John',
          lastName: 'Maverick',
        },
      },
    })
  },
),

Custom Predicate Support

  • HTTP and GraphQL handlers accept a custom predicate function as the first argument.
  • The predicate receives relevant request information and determines whether the handler should match the request.

Type Definitions

  • HTTP
    • The custom predicate receives request and cookies (not params, which was always {} and thus removed for clarity).
  • GraphQL
    • The custom predicate receives request, query, operationName, variables, and cookies.

Comment on lines 55 to 58
export type HttpCustomPredicate = (args: {
request: Request
cookies: Record<string, string>
}) => boolean | Promise<boolean>
Copy link
Contributor Author

@ytoshiki ytoshiki Jul 1, 2025

Choose a reason for hiding this comment

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

params is always an empty object when using a custom predicate because path is a function, so users cannot actually use it for any matching logic.

Copy link
Member

Choose a reason for hiding this comment

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

A good point. I wonder if we can do something to help with this. I think exporting a function that parses any request URL against any path would be nice. That way, if the developer is using a custom predicate function, they can still parse out the request url into params.

We have matchRequestUrl() exported from msw to do precisely that!

Copy link
Member

Choose a reason for hiding this comment

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

I am making the custom predicate return type a union of boolean or { match: boolean, params: PathParams } so the user can provide an extended predicate if they still wish to preserve path parameter parsing.

They should be able to do that using the matchRequestUrl function from MSW:

import { matchRequestUrl } from 'msw'

http.get(({ request }) => {
  return {
    matches: myCustomLogic,
    params: matchRequestUrl(request.url, '/:foo').params,
  }
})

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jul 1, 2025

Open in StackBlitz

npm i https://pkg.pr.new/msw@2541

commit: af5b4d6

@ytoshiki ytoshiki marked this pull request as ready for review July 1, 2025 15:13
src/core/http.ts Outdated
method: Method,
): HttpRequestHandler {
return (path, resolver, options = {}) => {
if (typeof path === 'function') {
Copy link
Member

Choose a reason for hiding this comment

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

What do you think if we handle this branching in the HttpHandler (if not RequestHandler) instead?

  1. Rename path argument to predicate.
  2. Handle it being a function in the .predicate() method.

This way, these namespace functions don't have to become more complex.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I completely agree. That is a much cleaner approach.

Copy link
Member

@kettanaito kettanaito left a comment

Choose a reason for hiding this comment

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

Hi, @ytoshiki. This looks amazing! I left a few comments, would love to hear your thoughts on them.

@kettanaito
Copy link
Member

One great way to reduce complexity in APIs that accept variable input (like our predicate) is to bring all that input to a single shared denominator. In our case, that would be the predicate function!

Basically, translate string paths into (path: string) => matchRequestUrl(path, ...) function calls. This way, for the underlying RequestHandler, it only ever deals with a single predicate function described by the child handler class (HTTP/GraphQL handlers).

Does this make sense?

@ytoshiki ytoshiki marked this pull request as draft July 3, 2025 11:36
@ytoshiki
Copy link
Contributor Author

ytoshiki commented Jul 3, 2025

One great way to reduce complexity in APIs that accept variable input (like our predicate) is to bring all that input to a single shared denominator. In our case, that would be the predicate function!

Basically, translate string paths into (path: string) => matchRequestUrl(path, ...) function calls. This way, for the underlying RequestHandler, it only ever deals with a single predicate function described by the child handler class (HTTP/GraphQL handlers).

Does this make sense?

Are you suggesting that the HTTP Handler's constructor should always create a predicate function like below?

this.predicateFunction = typeof predicate === 'function'
  ? predicate
  : (args) => {
      const hasMatchingMethod = this.matchMethod(args.request.method)
      const hasMatchingUrl = matchRequestUrl(new URL(args.request.url), predicate).matches
      return hasMatchingMethod && hasMatchingUrl
    }

So, the type check just moves from the predicate method to the constructor. the logic itself doesn't go away.
Is my understanding correct?

@ytoshiki ytoshiki marked this pull request as ready for review July 6, 2025 10:39
@ytoshiki
Copy link
Contributor Author

ytoshiki commented Jul 6, 2025

Hi, @ytoshiki. This looks amazing! I left a few comments, would love to hear your thoughts on them.

@kettanaito
Thank you for your review! I fixed them below.

As for caching, I commented below.

@ytoshiki ytoshiki requested a review from kettanaito July 12, 2025 13:12
@kettanaito
Copy link
Member

Thank you so much for your work, @ytoshiki. I will get to this as soon as I have a moment. Would love to get this released.

@kettanaito kettanaito changed the title feat: Custom request predicate function feat: custom request predicate function Aug 28, 2025
Copy link
Member

@kettanaito kettanaito left a comment

Choose a reason for hiding this comment

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

I've pushed a few minor changes here and there, otherwise this looks great!

@kettanaito kettanaito merged commit a05405b into mswjs:main Aug 30, 2025
20 checks passed
@kettanaito
Copy link
Member

Released: v2.11.0 🎉

This has been released in v2.11.0!

Make sure to always update to the latest version (npm i msw@latest) to get the newest features and bug fixes.


Predictable release automation by @ossjs/release.

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

Projects

None yet

2 participants