Skip to content

Commit

Permalink
feat: improve json body handling
Browse files Browse the repository at this point in the history
supporting body types like FormData and URLSearchParams

resolves #36
  • Loading branch information
pi0 committed Dec 13, 2021
1 parent baa3669 commit 4adb3bc
Show file tree
Hide file tree
Showing 5 changed files with 788 additions and 674 deletions.
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,16 @@
"devDependencies": {
"@nuxtjs/eslint-config-typescript": "latest",
"@types/flat": "latest",
"@types/mocha": "^9.0.0",
"@types/mocha": "latest",
"@types/node": "latest",
"@types/node-fetch": "^3.0.3",
"chai": "^4.3.4",
"eslint": "^7",
"chai": "latest",
"eslint": "latest",
"formdata-polyfill": "^4.0.10",
"h3": "latest",
"jiti": "latest",
"listhen": "latest",
"mocha": "^9.1.3",
"mocha": "latest",
"standard-version": "latest",
"typescript": "latest",
"unbuild": "latest"
Expand Down
17 changes: 8 additions & 9 deletions src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import destr from 'destr'
import { withBase, withQuery } from 'ufo'
import type { Fetch, RequestInfo, RequestInit, Response } from './types'
import { createFetchError } from './error'
import { isPayloadMethod, isJSONSerializable } from './utils'

export interface CreateFetchOptions { fetch: Fetch }

export type FetchRequest = RequestInfo

export interface SearchParams { [key: string]: any }

const payloadMethods = ['patch', 'post', 'put']

export interface FetchOptions extends Omit<RequestInit, 'body'> {
baseURL?: string
body?: RequestInit['body'] | Record<string, any>
Expand Down Expand Up @@ -49,8 +48,7 @@ export function createFetch ({ fetch }: CreateFetchOptions): $Fetch {
function onError (request: FetchRequest, opts: FetchOptions, error?: Error, response?: FetchResponse<any>): Promise<FetchResponse<any>> {
// Retry
if (opts.retry !== false) {
const hasPayload = payloadMethods.includes((opts.method || '').toLowerCase())
const retries = typeof opts.retry === 'number' ? opts.retry : (hasPayload ? 0 : 1)
const retries = typeof opts.retry === 'number' ? opts.retry : (isPayloadMethod(opts.method) ? 0 : 1)
if (retries > 0) {
return $fetchRaw(request, {
...opts,
Expand All @@ -76,11 +74,12 @@ export function createFetch ({ fetch }: CreateFetchOptions): $Fetch {
if (opts.params) {
request = withQuery(request, opts.params)
}
const hasPayload = payloadMethods.includes((opts.method || '').toLowerCase())
if (opts.body && typeof opts.body === 'object' && hasPayload) {
opts.body = JSON.stringify(opts.body)
setHeader(opts, 'content-type', 'application/json')
setHeader(opts, 'accept', 'application/json')
if (opts.body && isPayloadMethod(opts.method)) {
if (isJSONSerializable(opts.body)) {
opts.body = JSON.stringify(opts.body)
setHeader(opts, 'content-type', 'application/json')
setHeader(opts, 'accept', 'application/json')
}
}
}
const response: FetchResponse<any> = await fetch(request, opts as RequestInit).catch(error => onError(request, opts, error, undefined))
Expand Down
21 changes: 21 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const payloadMethods = new Set(Object.freeze(['PATCH', 'POST', 'PUT', 'DELETE']))
export function isPayloadMethod (method: string = 'GET') {
return payloadMethods.has(method.toUpperCase())
}

export function isJSONSerializable (val: any) {
if (val === undefined) {
return false
}
const t = typeof val
if (t === 'string' || t === 'number' || t === 'boolean' || t === null) {
return true
}
if (t !== 'object') {
return false // bigint, function, symbol, undefined
}
if (Array.isArray(val)) {
return true
}
return val.constructor?.name === 'Object' || typeof val.toJSON === 'function'
}
14 changes: 14 additions & 0 deletions test/index.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createApp, useBody } from 'h3'
import { expect } from 'chai'
import { Headers } from 'node-fetch'
import { $fetch } from 'ohmyfetch'
import { FormData } from 'formdata-polyfill/esm.min.js'

describe('ohmyfetch', () => {
let listener
Expand Down Expand Up @@ -58,6 +59,19 @@ describe('ohmyfetch', () => {
}
})

it('Bypass FormData body', async () => {
const data = new FormData()
data.append('foo', 'bar')
const { body } = await $fetch(getURL('post'), { method: 'POST', body: data })
expect(body).to.include('form-data; name="foo"')
})

it('Bypass URLSearchParams body', async () => {
const data = new URLSearchParams({ foo: 'bar' })
const { body } = await $fetch(getURL('post'), { method: 'POST', body: data })
expect(body).to.eq('foo=bar')
})

it('404', async () => {
const err = await $fetch(getURL('404')).catch(err => err)
expect(err.toString()).to.contain('404 Not Found')
Expand Down

0 comments on commit 4adb3bc

Please sign in to comment.