Skip to content

Instantly share code, notes, and snippets.

@yusukebe
Last active March 26, 2024 13:47
Show Gist options
  • Save yusukebe/180bfcf261d142df4e233e90e65cb63b to your computer and use it in GitHub Desktop.
Save yusukebe/180bfcf261d142df4e233e90e65cb63b to your computer and use it in GitHub Desktop.
type MethodOverrideOptions = {
// Default is 'form' and the value is `_method`
form?: string
header?: string
query?: string
}
const DEFAULT_METHOD_FORM_NAME = '_method'
const methodOverride = (options?: MethodOverrideOptions): MiddlewareHandler =>
async function methodOverride(c, next) {
// Method override by form
if (!options || options.form || !(options.form || options.header || options.query)) {
const methodFormName = options?.form || DEFAULT_METHOD_FORM_NAME
const contentType = c.req.header('content-type')
if (!(contentType === 'multipart/form-data' || contentType === 'application/x-www-form-urlencoded')) {
return await next()
}
const clonedRequest = c.req.raw.clone()
const newRequest = clonedRequest.clone()
const form = await clonedRequest.formData()
const method = form.get(methodFormName)
if (method) {
const newForm = await newRequest.formData()
newForm.delete(methodFormName)
const newHeaders = new Headers(clonedRequest.headers)
newHeaders.delete('content-type')
newHeaders.delete('content-length')
const request = new Request(c.req.url, {
body: newForm,
headers: newHeaders,
method
})
return app.fetch(request, c.env, c.executionCtx)
}
}
// Method override by header
else if (options.header) {
const headerName = options.header
const method = c.req.header(headerName)
if (method) {
const newHeaders = new Headers(c.req.raw.headers)
newHeaders.delete(headerName)
const request = new Request(c.req.raw, {
headers: newHeaders,
method
})
return app.fetch(request, c.env, c.executionCtx)
}
}
// Method override by query
else if (options.query) {
const queryName = options.query
const method = c.req.query(queryName)
if (method) {
const url = new URL(c.req.url)
url.searchParams.delete(queryName)
const request = new Request(url.toString(), {
body: c.req.raw.body,
headers: c.req.raw.headers,
method
})
return app.fetch(request, c.env, c.executionCtx)
}
}
await next()
}
@usualoma
Copy link

Hi @yusukebe
This is a surprisingly difficult middleware to implement. Here are some of my thoughts.

  • If you can override on GET, it may cause problems if you are using "SameSite=Lax" I think it is better not to override on GET.
  • When used with CSRF middleware, depending on the order in which csrf and methodOverride are specified, if 'content-type' is dropped in methodOverride, the csrf middleware may slip through unintentionally.

@yusukebe
Copy link
Author

Thanks @usualoma !

If you can override on GET, it may cause problems if you are using "SameSite=Lax" I think it is better not to override on GET.

That's right! I'll implement it as you said. For my use case, it's not a problem; it does not override on GET.

When used with CSRF middleware, depending on the order in which csrf and methodOverride are specified, if 'content-type' is dropped in methodOverride, the csrf middleware may slip through unintentionally.

I am removing the content-type header because if its value is starting multipart/form-data, the form content will not be created properly without removing it.

const form = new FormData()
form.append('foo', 'bar')
const req = new Request('http://localhost', {
  method: 'POST',
  body: form,
})

const newHeaders = new Headers(req.headers)
//newHeaders.delete('content-type')

const newForm = new FormData()
newForm.append('bar', 'baz')

const newReq = new Request('http://localhost', {
  method: 'DELETE',
  body: newForm,
  headers: newHeaders,
})

// If we don't remove `content-type`, it will be `null`
console.log((await newReq.formData()).get('bar')) // null

However, if we create a Request with a new FormData instance in the body, the new content-type is automatically added.

const form = new FormData()
form.append('foo', 'bar')
const req = new Request('http://localhost', {
  method: 'POST',
  body: form,
})
// multipart/form-data; boundary=----formdata-undici-008447172846
console.log(req.headers.get('content-type'))

const newHeaders = new Headers(req.headers)
newHeaders.delete('content-type')

const newForm = new FormData()
newForm.append('bar', 'baz')
const newReq = new Request('http://localhost', {
  method: 'DELETE',
  body: newForm,
  headers: newHeaders,
})

// multipart/form-data; boundary=----formdata-undici-060073812549
console.log(newReq.headers.get('content-type'))

The following line needs to be modified, but if the content-type starts with multipart/form-data, we may remove it.

if (!(contentType === 'multipart/form-data' || contentType === 'application/x-www-form-urlencoded')) {
  return await next()
}

@yusukebe
Copy link
Author

Either way, I would like this feature and would like to make a PR.

@usualoma
Copy link

Thanks for the explanation about the content-type, I understand. Thanks!

@yusukebe
Copy link
Author

@usualoma

Created the PR! honojs/hono#2420

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