Aller au contenu

Fetch et JavaScript⚓︎

Une page dédiée ?

Malheureusement, oui, fetch() a besoin d’une page dédiée car cette fonctionnalité — bien que récente — n’est vraiment pas bien finie…

💱 Sérialisation des données⚓︎

Un article de Baldur Bjarnason explique comment utiliser FormData pour sérialiser les données qui proviennent d’un formulaire, soit pour du classique urlencoded soit pour du JSON.

fetch-serialize-form.js
const formdata = new FormData(form)
fetch('/test/thing', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: new URLSearchParams(formdata).toString(),
})
  .then((result) => {
    // do something
  })
  .catch((err) => {
    // fix something.
  })
fetch-serialize-json.js
const formdata = new FormData(form)
fetch('/test/thing', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.serialise(Object.fromEntries(formdata.entries())),
})
  .then((result) => {
    // do something
  })
  .catch((err) => {
    // fix something.
  })

Vérifier la nature de la réponse d’erreur d’une API⚓︎

Une astuce de Chris Ferdinandi pour évaluer la bonne erreur à afficher lorsqu’elle provient d’une API, on utilise le header de content-type pour déterminer la méthode à appeler sur l’erreur.

fetch-api-error.js
fetch('https://jsonplaceholder.typicode.com/tododos')
  .then(function (response) {
    if (response.ok) {
      return response.json()
    }
    throw response
  })
  .then(function (data) {
    console.log(data)
  })
  .catch(function (error) {
    // Check if the response is JSON or not
    let isJSON = error.headers
      .get('content-type')
      .includes('application/json')

    // If JSON, use text(). Otherwise, use json().
    let getMsg = isJSON ? error.json() : error.text()

    // Warn the error and message when it resolves
    getMsg.then(function (msg) {
      console.warn(error, msg)
    })
  })

Choisir/deviner les formats en entrée/sortie⚓︎

Un autre extrait d’un ancien projet où on voulait pouvoir envoyer un hash ou un FormData et récupérer du JSON ou du texte.

fetch-handle-formats.js
function request(url, options = undefined) {
  return fetch(url, optionsHandler(options))
    .then(handleResponse)
    .catch((error) => {
      const e = new Error(`${error.message} ${url}`)
      Object.assign(e, error, { url })
      throw e
    })
}

function optionsHandler(options) {
  const def = {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' },
  }

  if (!options) return def

  let r = Object.assign({}, def, options)

  // Deal with body, can be either a hash or a FormData,
  // will generate a JSON string from it if in options.
  delete r.body
  if (options.body) {
    // Allow to pass an empty hash too.
    if (!(Object.getOwnPropertyNames(options.body).length === 0)) {
      r.body = JSON.stringify(options.body)
    } else if (options.body instanceof FormData) {
      r.body = JSON.stringify(Array.from(options.body.entries()))
    }
  }
  return r
}

const handlers = {
  JSONResponseHandler(response) {
    return response.json().then((json) => {
      if (response.ok) {
        return json
      } else {
        return Promise.reject(
          Object.assign({}, json, {
            status: response.status,
          })
        )
      }
    })
  },
  textResponseHandler(response) {
    if (response.ok) {
      return response.text()
    } else {
      return Promise.reject(
        Object.assign(
          {},
          {
            status: response.status,
            message: response.statusText,
          }
        )
      )
    }
  },
}

function handleResponse(response) {
  let contentType = response.headers.get('content-type')
  if (contentType.includes('application/json')) {
    return handlers.JSONResponseHandler(response)
  } else if (contentType.includes('text/html')) {
    return handlers.textResponseHandler(response)
  } else {
    throw new Error(`
      Sorry, content-type '${contentType}' is not supported,
      only 'application/json' and 'text/html' are.
    `)
  }
}

En entrée, on redéfinit le body de notre requête en fonction de sa nature (détectée automagiquement). En sortie, on regarde le content-type pour appliquer le handler approprié.

Retourner l’URL qui a causé l’erreur⚓︎

On voudrait par exemple afficher le domaine qui a causé l’erreur, étonnamment ça n’est pas trivial :

fetch-url-error.js
class ServerError extends Error {}

function request(url, options) {
  return fetch(url, options)
    .then((response) => [response, response.json()])
    .then(([response, data]) => {
      if (response.ok) {
        return data
      } else {
        const e = new ServerError(`${url} ${response.status} ${data}`)
        e.status = response.status
        e.url = url
        e.data = data
        throw e
      }
    })
    .catch((error) => {
      if (error instanceof ServerError) throw error
      const e = new Error(`${error.message} ${url}`)
      e.url = url
      throw e
    })
}

function handleError(error) {
  console.error(error)
  const errorURL = new window.URL(error.url)
  const userMessage = `
    Le domaine ${errorURL.host} semble être inaccessible.
    Nous en avons été informés, veuillez réessayer plus tard.
  `
}

Que l’on va ensuite pouvoir utiliser ainsi :

1
2
3
4
5
request(url)
  .then(response => {
    // Do something.
  })
  .catch(handleError)

Ce qui permettra d’afficher une erreur personnalisée contenant le domaine concerné sans avoir à passer l’URL dans la méthode qui gère les erreurs.

Aller plus loin/différemment⚓︎


Dernière mise à jour: 2022-08-16