body.spec.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. import type { IncomingMessage } from 'node:http'
  2. import { describe, expect, it, vi } from 'vitest'
  3. import { readBoundedUtf8Body } from '../src/body.ts'
  4. /** Minimal async-iterable request for byte-level branches Node fetch cannot construct. */
  5. function request(options: {
  6. chunks?: Array<Buffer | string>
  7. contentLength?: string
  8. complete?: boolean
  9. error?: unknown
  10. } = {}): IncomingMessage & { resume: ReturnType<typeof vi.fn> } {
  11. const resume = vi.fn()
  12. return {
  13. headers: {
  14. ...(options.contentLength === undefined ? {} : { 'content-length': options.contentLength }),
  15. },
  16. complete: options.complete ?? true,
  17. resume,
  18. async * [Symbol.asyncIterator]() {
  19. for (const chunk of options.chunks ?? []) yield chunk
  20. if (options.error !== undefined) throw options.error
  21. },
  22. } as unknown as IncomingMessage & { resume: ReturnType<typeof vi.fn> }
  23. }
  24. describe('bounded webhook body intake', () => {
  25. it('accepts an absent length and both Buffer and string chunks', async () => {
  26. await expect(readBoundedUtf8Body(request({ chunks: [Buffer.from('{'), '}'] }), 2)).resolves.toBe('{}')
  27. })
  28. it('rejects malformed, unsafe, and oversized declared lengths', async () => {
  29. await expect(readBoundedUtf8Body(request({ contentLength: '01' }), 10)).rejects.toMatchObject({ status: 400 })
  30. await expect(readBoundedUtf8Body(request({ contentLength: '999999999999999999999' }), Number.MAX_SAFE_INTEGER))
  31. .rejects.toMatchObject({ status: 413 })
  32. const oversized = request({ contentLength: '3' })
  33. await expect(readBoundedUtf8Body(oversized, 2)).rejects.toMatchObject({ status: 413 })
  34. expect(oversized.resume).toHaveBeenCalledOnce()
  35. })
  36. it('rejects a chunked body at the first byte beyond the cap', async () => {
  37. const streamed = request({ chunks: [Buffer.from('ab'), Buffer.from('c')] })
  38. await expect(readBoundedUtf8Body(streamed, 2)).rejects.toMatchObject({ status: 413 })
  39. expect(streamed.resume).toHaveBeenCalledOnce()
  40. })
  41. it('normalizes stream failure and incomplete EOF as an aborted body', async () => {
  42. await expect(readBoundedUtf8Body(request({ error: new Error('socket') }), 10))
  43. .rejects.toMatchObject({ status: 400, message: 'request body was aborted' })
  44. await expect(readBoundedUtf8Body(request({ complete: false }), 10))
  45. .rejects.toMatchObject({ status: 400, message: 'request body was aborted' })
  46. })
  47. it('rejects invalid UTF-8 after a complete bounded read', async () => {
  48. await expect(readBoundedUtf8Body(request({ chunks: [Buffer.from([0xff])] }), 1))
  49. .rejects.toMatchObject({ status: 400, message: 'request body is not valid UTF-8' })
  50. })
  51. })