unjs / ofetch

😱 A better fetch API. Works on node, browser and workers.
MIT License
4.16k stars 129 forks source link

ParamSerializer for onRequest (interceptor) destructing string (ufo.withQuery) #119

Open pitcher-maarten opened 2 years ago

pitcher-maarten commented 2 years ago

As Axios does have the paramsSerializer I wanted to recreate this behaviour in combination with qs. To achieve this I've created an $fetch instance:

export const apiConfig = {
    baseURL: 'https://jsonplaceholder.typicode.com',
    onRequest: ({ options }) => {
        options.params = qs.stringify(options.params, { arrayFormat: 'comma', encode: false  })
    }
}

The request params:

sortBy: "test",
test: {
    foo: "bar",
    arr: [1, 2, 3],
}

After stringify options.params results in:

sortBy=test&test[foo]=bar&test[arr]=1,2,3

The request by ohmyfetch:

https://jsonplaceholder.typicode.com/todos?0=s&1=o&2=r&3=t&4=B&5=y&6==&7=t&8=e&9=s&10=t&11=%26&12=t&13=e&14=s&15=t&16=[&17=f&18=o&19=o&20=]&21==&22=b&23=a&24=r&25=%26&26=t&27=e&28=s&29=t&30=[&31=a&32=r&33=r&34=]&35==&36=1&37=,&38=2&39=,&40=3

As you can see the params are getting destructed by withQuery https://github.com/unjs/ohmyfetch/blob/main/src/fetch.ts#L109

This issue is similar to: https://github.com/unjs/ohmyfetch/issues/117

engineers-dope commented 2 years ago

I'm facing similar problem updating to Nuxt 3 rc 11. My queries become unusable and had to revert back. Can we skip withQuery to resolve this issue or does anyone have any ideas?

lukas-pierce commented 1 year ago

Make wrapper function for fetch:

import { ofetch } from 'ofetch'

const myQuerySerializer = (params) => {
  // Use your favorite serializer:
  // https://www.npmjs.com/package/qs
  // http://api.jquery.com/jquery.param/
  return '?serialized=params'
}

/**
 * Accept fetch instance and wrap it with custom function,
 * which make some modifications before execute request
 *
 * Returns function, same signature as original fetch
 */
function wrapFetch (fetch) {
  return (request, options) => {

    // before
    console.log('original request', {
      request, // string
      options, // { params: {...} }
    })

    // modify request if has params in options
    let modifiedRequest, modifiedOptions
    if (options && options.params) {
      // append params string and delete params from options
      modifiedRequest = request + myQuerySerializer(options.params)
      modifiedOptions = _.omit(options, ['params'])
    } else {
      // use as is
      modifiedRequest = request
      modifiedOptions = options
    }

    // after
    console.log('modified request', {
      request: modifiedRequest,
      options: modifiedOptions,
    })

    return fetch(modifiedRequest, modifiedOptions)
  }
}

class Api {
  constructor (fetch) {
    this.fetch = wrapFetch(fetch)
  }

  getPosts (params = {}) {
    return this.fetch('/post', { params })
  }
}

const apiFetch = ofetch.create({
  baseURL: '/api',
  onRequest ({ options }) {
    options.headers = {
      Authorization: 'Bearer ...' // bonus 😃
    }
  },
})

// api usage
const api = new Api(apiFetch)
const posts = await api.getPosts({
  // this params will be serialized by myQuerySerializer
  param1: 'value1',
  param2: 'value2',
})
console.log('posts:', posts)
image
vedmant commented 1 year ago

@lukas-pierce That would work, but looks like a workaround, is there no proper support of custom query serializers?

lukas-pierce commented 1 year ago

@lukas-pierce That would work, but looks like a workaround, is there no proper support of custom query serializers?

No, I checked original library sources, need request to library author

karombekHusanov commented 4 months ago

qs.stringify(options.params, { arrayFormat: 'comma', encode: false })

Yeah, this works even you can simplify the wrapper function, but still this is not the real solution, it is workaround, unfortunately ofetch does not support this feature yet, hope it will soon

alexdeia commented 3 months ago

You can use a simple function to do this

Prerequisites:

Serializer

function serializeQuery(query: Record<string, any>) {
  const queryString = qs.stringify(query, { encode: false })

  return queryString.split('&').reduce<Record<string, any>>((acc, part) => {
    const [key, value] = part.split('=')

    acc[key] = value

    return acc
  }, {})
}

ofetch instance

ofetch.create({
  baseURL: import.meta.env.VITE_APP_VOCABS_API,
  async onRequest({ options }) {
    if (options.query)
      options.query = serializeQuery(options.query)
  },
})

How to use

ofetchInstance('/api', {
  query: {
    where: {
      key: { equals: 'foo' },
    },
    limit: 0,
  },
})

As a result

{
    where: {
      key: { equals: 'foo' },
    },
    limit: 0,
  }

serialized to ?where[key][equals]=foo&limit=0