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

RFC: Request handler API #290

Closed
kettanaito opened this issue Jul 23, 2020 · 4 comments · Fixed by mswjs/mswjs.io#89
Closed

RFC: Request handler API #290

kettanaito opened this issue Jul 23, 2020 · 4 comments · Fixed by mswjs/mswjs.io#89
Assignees
Labels
help wanted Extra attention is needed needs:discussion

Comments

@kettanaito
Copy link
Member

What

Need to refine an existing Request handler API to be publicly usable.

The Request handler API is already designed with the support of creating custom request handlers for about any purpose (support of a specific communication convention, embed application's logic into handlers, etc.). However, if you take a look at the implementation example on the library's side, it uses quite a few internal functions, which may be useful to expose in the public API:

  • getCleanUrl
  • isStringEqual
  • match
  • The entire log() function.

Instead of proxying single functions from internal/external packages, MSW should expose a high-level API that can help developers build their own request handlers. Such high-level API may include:

  • matchesUrl(), that would clean and match two given URLs just as MSW does for REST API.
  • log(), a ready-to-use logging function (call signature preserved).

Motivation

  • Support CalDAV methods #287 (support for CalDAV methods)
  • Allows developers to create custom request handlers for internal and public usage (i.e. publishing in an NPM package):
import { websockets } from 'msw-websockets-handlers'
import { setupWorker } from 'msw'

setupWorker(
  websockets.on('message', () => {...})
)

Current API

Type declaration

export interface RequestHandler<
RequestType = MockedRequest,
ContextType = typeof defaultContext,
ParsedRequest = any,
PublicRequest = RequestType
> {
/**
* Parses a captured request to retrieve additional
* information meant for internal usage in the request handler.
*/
parse?: (req: MockedRequest) => ParsedRequest | null
/**
* Returns a modified request with necessary public properties appended.
*/
getPublicRequest?: (
req: RequestType,
parsedRequest: ParsedRequest,
) => PublicRequest
/**
* Predicate function that decides whether a Request should be mocked.
*/
predicate: (req: RequestType, parsedRequest: ParsedRequest) => boolean
/**
* Returns a mocked response object to the captured request.
*/
resolver: ResponseResolver<RequestType, ContextType>
/**
* Returns a map of context utility functions available
* under the `ctx` argument of request handler.
*/
defineContext?: (req: PublicRequest) => ContextType
/**
* Prints out a mocked request/response information
* upon each request capture into browser's console.
*/
log: (
req: PublicRequest,
res: ResponseWithSerializedHeaders,
handler: RequestHandler<RequestType, ContextType>,
parsedRequest: ParsedRequest,
) => void
/**
* Describes whether this request handler should be skipped
* when dealing with any subsequent matching requests.
*/
shouldSkip?: boolean
}

Implementation example

msw/src/rest.ts

Lines 50 to 147 in 6001b79

const createRestHandler = (method: RESTMethods) => {
return (
mask: Mask,
resolver: ResponseResolver<MockedRequest, typeof restContext>,
): RequestHandler<MockedRequest, typeof restContext> => {
const resolvedMask = resolveMask(mask)
const cleanMask =
resolvedMask instanceof URL
? getCleanUrl(resolvedMask)
: resolvedMask instanceof RegExp
? resolvedMask
: resolveRelativeUrl(resolvedMask)
return {
predicate(req) {
// Ignore query parameters and hash when matching requests URI
const cleanUrl = getCleanUrl(req.url)
const hasSameMethod = isStringEqual(method, req.method)
const urlMatch = match(cleanMask, cleanUrl)
return hasSameMethod && urlMatch.matches
},
getPublicRequest(req) {
// Get request path parameters based on the given mask
const params =
(mask &&
match(resolveRelativeUrl(mask), getCleanUrl(req.url)).params) ||
{}
return {
...req,
params,
}
},
resolver,
defineContext() {
return restContext
},
log(req, res, handler) {
// Warn on request handler URL containing query parameters.
if (resolvedMask instanceof URL && resolvedMask.search !== '') {
const queryParams: string[] = []
resolvedMask.searchParams.forEach((_, paramName) =>
queryParams.push(paramName),
)
console.warn(
`\
[MSW] Found a redundant usage of query parameters in the request handler URL for "${method} ${mask}". Please match against a path instead, and access query parameters in the response resolver function:
rest.${method.toLowerCase()}("${resolvedMask.pathname}", (req, res, ctx) => {
const query = req.url.searchParams
${queryParams
.map(
(paramName) => `\
const ${paramName} = query.get("${paramName}")`,
)
.join('\n')}
})\
`,
)
}
const isRelativeRequest = req.referrer.startsWith(req.url.origin)
const publicUrl = isRelativeRequest
? req.url.pathname
: format({
protocol: req.url.protocol,
host: req.url.host,
pathname: req.url.pathname,
})
const loggedRequest = prepareRequest(req)
const loggedResponse = prepareResponse(res)
console.groupCollapsed(
'[MSW] %s %s %s (%c%s%c)',
getTimestamp(),
req.method,
publicUrl,
`color:${getStatusCodeColor(res.status)}`,
res.status,
'color:inherit',
)
console.log('Request', loggedRequest)
console.log('Handler:', {
mask,
resolver: handler.resolver,
})
console.log('Response', loggedResponse)
console.groupEnd()
},
}
}
}

@marcosvega91
Copy link
Member

marcosvega91 commented Jul 25, 2020

I think that the features that you list could be enough to create custom libraries.
I was thinking about websocket and It would be a great feature to add to MSW. I'm only afraid that websocket can't be intercepted by service worker 😢

@kettanaito
Copy link
Member Author

@marcosvega91, yeap, we've got the WebSocket feature request opened here #156.

@kettanaito
Copy link
Member Author

kettanaito commented Aug 5, 2020

With #319 the request matching API is now publicly exposed as matchRequestUrl. This may help developers in building their custom request handler while reusing the same request matching logic as MSW uses internally.

@kettanaito
Copy link
Member Author

kettanaito commented Oct 23, 2020

We may have to expose a few internal functions (i.e. default loggers) for a more comfortable creation of custom handlers, but I find the current state of the request handler API sufficient for most purposes:

export interface RequestHandler<
RequestType = MockedRequest,
ContextType = typeof defaultContext,
ParsedRequest = any,
PublicRequest = RequestType,
ResponseBodyType = any
> {
/**
* Parses a captured request to retrieve additional
* information meant for internal usage in the request handler.
*/
parse?: (req: RequestType) => ParsedRequest | null
/**
* Returns a modified request with necessary public properties appended.
*/
getPublicRequest?: (
req: RequestType,
parsedRequest: ParsedRequest,
) => PublicRequest
/**
* Predicate function that decides whether a Request should be mocked.
*/
predicate: (req: RequestType, parsedRequest: ParsedRequest) => boolean
/**
* Returns a mocked response object to the captured request.
*/
resolver: ResponseResolver<RequestType, ContextType, ResponseBodyType>
/**
* Returns a map of context utility functions available
* under the `ctx` argument of request handler.
*/
defineContext?: (req: PublicRequest) => ContextType
/**
* Prints out a mocked request/response information
* upon each request capture into browser's console.
*/
log: (
req: PublicRequest,
res: ResponseWithSerializedHeaders,
handler: RequestHandler<
RequestType,
ContextType,
ParsedRequest,
PublicRequest,
ResponseBodyType
>,
parsedRequest: ParsedRequest,
) => void
/**
* Describes whether this request handler should be skipped
* when dealing with any subsequent matching requests.
*/
shouldSkip?: boolean
}

I will document this in the recipe and close this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed needs:discussion
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants