Добавление заголовков

HTTP-заголовки позволяют передавать дополнительную информацию вместе с запросом. В RTK Query механизм добавления заголовков используется для:

  • авторизации;
  • передачи токенов;
  • указания типа контента;
  • локализации;
  • работы с API-ключами;
  • кэширования;
  • настройки пользовательских параметров запроса.

RTK Query предоставляет несколько способов добавления заголовков:

  • глобально через prepareHeaders;
  • локально внутри конкретного endpoint;
  • динамически на основе состояния Redux;
  • через пользовательский baseQuery.

Добавление заголовков через prepareHeaders

Наиболее распространённый способ — использование функции prepareHeaders внутри fetchBaseQuery.

Пример базовой конфигурации:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const api = createApi({
    reducerPath: 'api',

    baseQuery: fetchBaseQuery({
        baseUrl: 'https://api.example.com',

        prepareHeaders: (headers) => {
            headers.set('Content-Type', 'application/json')
            headers.set('Accept', 'application/json')

            return headers
        }
    }),

    endpoints: () => ({})
})

В данном случае заголовки будут автоматически добавляться ко всем запросам API.


Заголовок авторизации

Чаще всего заголовки используются для передачи JWT-токена.

Пример:

prepareHeaders: (headers) => {
    const token = localStorage.getItem('token')

    if (token) {
        headers.set('Authorization', `Bearer ${token}`)
    }

    return headers
}

После этого каждый запрос будет содержать:

Authorization: Bearer eyJhbGciOi...

Получение токена из Redux Store

RTK Query позволяет получать доступ к состоянию Redux внутри prepareHeaders.

Пример:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const api = createApi({
    reducerPath: 'api',

    baseQuery: fetchBaseQuery({
        baseUrl: 'https://api.example.com',

        prepareHeaders: (headers, { getState }) => {
            const token = getState().auth.token

            if (token) {
                headers.set('Authorization', `Bearer ${token}`)
            }

            return headers
        }
    }),

    endpoints: () => ({})
})

Второй аргумент prepareHeaders

Функция prepareHeaders получает объект с дополнительной информацией:

prepareHeaders: (headers, api) => {
    console.log(api)

    return headers
}

Доступные свойства:

{
    getState,
    endpoint,
    type,
    forced,
    extra
}

endpoint

Имя текущего endpoint:

prepareHeaders: (headers, { endpoint }) => {
    console.log(endpoint)

    return headers
}

type

Тип запроса:

prepareHeaders: (headers, { type }) => {
    console.log(type)
}

Возможные значения:

'query'
'mutation'

forced

Показывает, был ли запрос принудительно обновлён.

prepareHeaders: (headers, { forced }) => {
    console.log(forced)

    return headers
}

Условное добавление заголовков

Иногда разные endpoint требуют разные заголовки.

Пример:

prepareHeaders: (headers, { endpoint }) => {
    if (endpoint === 'uploadFile') {
        headers.set('X-Upload-Mode', 'true')
    }

    return headers
}

Добавление API-ключа

Многие внешние сервисы используют API-ключи.

Пример:

prepareHeaders: (headers) => {
    headers.set('X-API-Key', 'my-secret-key')

    return headers
}

Добавление пользовательских заголовков

API может требовать произвольные заголовки.

Пример:

prepareHeaders: (headers) => {
    headers.set('X-App-Version', '1.0.0')
    headers.set('X-Client', 'web')

    return headers
}

Локализация запросов

Некоторые backend-сервисы поддерживают мультиязычность через заголовок Accept-Language.

Пример:

prepareHeaders: (headers) => {
    headers.set('Accept-Language', 'ru')

    return headers
}

Добавление заголовков внутри endpoint

Заголовки можно указывать непосредственно внутри запроса.

Пример:

getUsers: builder.query({
    query: () => ({
        url: '/users',

        headers: {
            'X-Request-Source': 'admin-panel'
        }
    })
})

Динамические заголовки в endpoint

Заголовки могут зависеть от аргументов запроса.

Пример:

getUser: builder.query({
    query: (id) => ({
        url: `/users/${id}`,

        headers: {
            'X-User-ID': id
        }
    })
})

Комбинирование глобальных и локальных заголовков

RTK Query объединяет заголовки из prepareHeaders и endpoint.

Пример:

baseQuery: fetchBaseQuery({
    baseUrl: '/api',

    prepareHeaders: (headers) => {
        headers.set('Authorization', 'Bearer token')

        return headers
    }
})

Endpoint:

getPosts: builder.query({
    query: () => ({
        url: '/posts',

        headers: {
            'X-Module': 'blog'
        }
    })
})

Результирующий запрос:

Authorization: Bearer token
X-Module: blog

Переопределение заголовков

Локальные заголовки могут переопределять глобальные.

Пример:

prepareHeaders: (headers) => {
    headers.set('Content-Type', 'application/json')

    return headers
}

Endpoint:

uploadFile: builder.mutation({
    query: (formData) => ({
        url: '/upload',
        method: 'POST',
        body: formData,

        headers: {
            'Content-Type': 'multipart/form-data'
        }
    })
})

Работа с FormData

При использовании FormData важно учитывать особенности браузера.

Неправильный вариант:

headers.set('Content-Type', 'multipart/form-data')

Браузер должен самостоятельно формировать boundary.

Правильный вариант:

uploadFile: builder.mutation({
    query: (formData) => ({
        url: '/upload',
        method: 'POST',
        body: formData
    })
})

Удаление заголовков

Объект headers поддерживает метод delete.

Пример:

prepareHeaders: (headers, { endpoint }) => {
    if (endpoint === 'publicData') {
        headers.delete('Authorization')
    }

    return headers
}

Проверка существования заголовка

Метод has позволяет проверить наличие заголовка.

Пример:

prepareHeaders: (headers) => {
    if (!headers.has('Content-Type')) {
        headers.set('Content-Type', 'application/json')
    }

    return headers
}

Чтение текущего значения заголовка

Пример:

prepareHeaders: (headers) => {
    console.log(headers.get('Authorization'))

    return headers
}

Для работы с cookie необходимо включить credentials.

Пример:

baseQuery: fetchBaseQuery({
    baseUrl: '/api',

    credentials: 'include'
})

Теперь браузер будет автоматически отправлять cookie вместе с запросами.


Настройка credentials

Возможные значения:

'omit'
'same-origin'
'include'

Пример:

baseQuery: fetchBaseQuery({
    baseUrl: '/api',
    credentials: 'same-origin'
})

Добавление заголовков в mutation

Пример:

createPost: builder.mutation({
    query: (post) => ({
        url: '/posts',
        method: 'POST',
        body: post,

        headers: {
            'X-Action': 'create-post'
        }
    })
})

Добавление заголовков в query

Пример:

getPosts: builder.query({
    query: () => ({
        url: '/posts',

        headers: {
            'Cache-Control': 'no-cache'
        }
    })
})

Централизация логики заголовков

Крупные приложения обычно выносят логику заголовков в отдельные функции.

Пример:

const setAuthHeaders = (headers, token) => {
    if (token) {
        headers.set('Authorization', `Bearer ${token}`)
    }

    return headers
}

Использование:

prepareHeaders: (headers, { getState }) => {
    const token = getState().auth.token

    return setAuthHeaders(headers, token)
}

Использование нескольких токенов

Иногда приложение использует разные токены для разных сервисов.

Пример:

prepareHeaders: (headers, { endpoint, getState }) => {
    const state = getState()

    if (endpoint.startsWith('admin')) {
        headers.set(
            'Authorization',
            `Bearer ${state.admin.token}`
        )
    } else {
        headers.set(
            'Authorization',
            `Bearer ${state.user.token}`
        )
    }

    return headers
}

Пользовательский baseQuery

Для сложной логики можно создать собственный baseQuery.

Пример:

import { fetchBaseQuery } from '@reduxjs/toolkit/query'

const rawBaseQuery = fetchBaseQuery({
    baseUrl: '/api'
})

const customBaseQuery = async (args, api, extraOptions) => {
    const token = api.getState().auth.token

    if (typeof args === 'string') {
        args = {
            url: args
        }
    }

    args.headers = {
        ...args.headers,
        Authorization: `Bearer ${token}`
    }

    return rawBaseQuery(args, api, extraOptions)
}

Использование:

export const api = createApi({
    reducerPath: 'api',
    baseQuery: customBaseQuery,

    endpoints: () => ({})
})

Автоматическое обновление токена

Часто baseQuery используется для обновления access token после получения ошибки 401.

Пример структуры:

const baseQuery = fetchBaseQuery({
    baseUrl: '/api'
})

const baseQueryWithReauth = async (
    args,
    api,
    extraOptions
) => {
    let result = await baseQuery(args, api, extraOptions)

    if (result.error?.status === 401) {
        const refreshResult = await baseQuery(
            '/refresh',
            api,
            extraOptions
        )

        if (refreshResult.data) {
            api.dispatch(setToken(refreshResult.data.token))

            result = await baseQuery(
                args,
                api,
                extraOptions
            )
        }
    }

    return result
}

Ошибки при работе с заголовками

Отсутствие return headers

Неправильный вариант:

prepareHeaders: (headers) => {
    headers.set('Authorization', 'Bearer token')
}

Правильный вариант:

prepareHeaders: (headers) => {
    headers.set('Authorization', 'Bearer token')

    return headers
}

Жёстко захардкоженный токен

Плохая практика:

headers.set('Authorization', 'Bearer 123456')

Токен должен храниться:

  • в Redux;
  • cookie;
  • localStorage;
  • sessionStorage.

Установка multipart/form-data

Одна из самых распространённых ошибок:

headers.set('Content-Type', 'multipart/form-data')

Это приводит к отсутствию boundary и поломке загрузки файлов.


Рекомендации по архитектуре

Использование одного API-слоя

Обычно создаётся единый API-модуль:

export const api = createApi({
    reducerPath: 'api',
    baseQuery,
    endpoints: () => ({})
})

Разделение публичных и приватных запросов

Пример:

prepareHeaders: (headers, { endpoint }) => {
    const publicEndpoints = [
        'login',
        'register'
    ]

    if (!publicEndpoints.includes(endpoint)) {
        headers.set('Authorization', 'Bearer token')
    }

    return headers
}

Выделение конфигурации в отдельные файлы

Пример структуры:

src/
├── app/
├── services/
│   ├── api.js
│   ├── baseQuery.js
│   └── headers.js

Такой подход упрощает поддержку и повторное использование логики.