MIME-типы: что это и зачем серверу Content-Type

8 ноября 2026 г.

Картинка не вставляется на страницу, а в консоли браузера появляется строка про «resource interpreted as image but transferred with MIME type application/octet-stream». Сайт при этом работает, расширение у файла правильное, и непонятно, за что прицепился браузер. Зацепился он за заголовок ответа сервера, и это MIME-тип, одна из самых недооценённых деталей веба: до неё редко доходят руки, пока что-нибудь не перестанет отрисовываться.

Что такое MIME-тип

MIME-тип это метка формата данных, записанная как тип/подтип. Запись text/html означает HTML-документ, image/png картинку в формате PNG, application/json данные в JSON. Название осталось от электронной почты: сокращение расшифровывается как Multipurpose Internet Mail Extensions, стандарт описывал, как вкладывать в письма не только текст, а веб позаимствовал саму схему меток. Дополнительные параметры пишутся через точку с запятой после типа, самый частый из них кодировка: text/html; charset=utf-8.

Живёт эта метка в заголовке ответа Content-Type: сервер отдаёт не только тело файла, но и строчку, в которой обещает, что внутри. Именно по этой строчке браузер решает, что делать с содержимым: нарисовать, применить как стиль, исполнить как скрипт, показать как текст или предложить скачать. Тот же механизм работает в письмах и внутри multipart-сообщений, где каждая часть помечена своим типом.

Тот же заголовок работает и в обратную сторону: клиент, отправляющий данные, тоже объявляет их формат. POST-запрос с JSON несёт Content-Type: application/json, обычная отправка формы идёт с application/x-www-form-urlencoded, а файлы из браузера уезжают в multipart/form-data. Сервер читает заголовок запроса так же, как браузер читает заголовок ответа, и неверный тип ломает разбор на принимающей стороне ничуть не мягче, чем неверный тип ответа.

Ответ сервера с заголовком Content-Type
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 5412

<!DOCTYPE html>
<html lang="ru">
...

Основные группы типов

  • text/: человекочитаемый текст, сюда входят html, css, javascript и plain text;
  • image/: растровые и векторные картинки, png, jpeg, gif, svg, webp;
  • application/: бинарные и прикладные форматы, json, pdf, zip, шрифты;
  • audio/ и video/: медиафайлы, mp3, mp4, webm;
  • multipart/: сообщения из нескольких частей, типично загрузка форм с файлами;
  • font/: шрифтовые форматы woff, woff2, хотя их часто отдают и как application/.

Почему браузер не угадывает формат

Расширение файла, на которое все полагаются, до браузера не доезжает: в URL оно может быть любым, а может и отсутствовать вовсе. Единственный официальный источник правды о формате это Content-Type ответа. Когда заголовка нет или он бессодержательный, браузер включает MIME sniffing: читает первые байты файла и гадает по ним, что внутри. Гадание срабатывает на простых случаях и проваливается на спорных, поэтому протокол предусматривает и жёсткий режим: заголовок X-Content-Type-Options: nosniff запрещает браузеру угадывать и заставляет верить серверу как есть.

PNG без расширения наглядно показывает оба поведения. Если сервер отдал файл с честным image/png, картинка откроется независимо от имени файла. Если отдал как application/octet-stream, браузер вместо показа предложит скачать. А если не дал заголовка вовсе, включится угадывание по сигнатуре формата, и результат будет зависеть от настроек конкретного браузера.

Что ломается при неверном MIME

  • CSS отдали как text/plain: при включённом nosniff браузер откажется применять таблицу стилей, и страница рассыпается без оформления;
  • скрипт отдали как text/plain: с nosniff исполнение блокируется, в консоли появляется ошибка про MIME type;
  • JSON отдали как text/html: клиенты и утилиты, которые доверяют заголовку, откажутся парсить ответ, а отладочные панели не покажут структуру;
  • SVG отдали как application/octet-stream: тег img не отрисует такую картинку и покажет заглушку;
  • woff2 отдали как text/plain: при nosniff шрифт не загрузится, и текст поедет системным начертанием;
  • HTML отдали без charset: браузер начнёт угадывать кодировку, и кириллица превращается в кракозябры, подробнее об этом в статье про UTF-8 и Unicode.
Неверный и верный Content-Type для одного файла
Файл: styles.css

Неверно:  Content-Type: text/plain
Результат: с nosniff стили не применяются

Верно:    Content-Type: text/css; charset=utf-8
Результат: стили применяются, вопросы сняты

Минимальный набор типов для сайта

  • .html → text/html, обязательно с charset=utf-8;
  • .css → text/css;
  • .js → text/javascript, современный стандарт;
  • .json → application/json;
  • .png → image/png;
  • .jpg и .jpeg → image/jpeg;
  • .svg → image/svg+xml;
  • .webp → image/webp;
  • .pdf → application/pdf;
  • .txt → text/plain;
  • .woff2 → font/woff2;
  • .mp4 → video/mp4.

Строка .html с обязательным charset выглядит очевидной, но именно её чаще всего забывают в самописных обработчиках: страницу отдают голым text/html без указания кодировки, и дальше работает угадывание браузера со всеми его сюрпризами. Второе по частоте место это .json: небольшие самодельные серверы шлют по умолчанию text/html или application/octet-stream, и клиентская библиотека на другом конце получает ответ, который отказывается разбирать.

Полный реестр ведёт IANA, и он исчисляется тысячами записей, но перечисленного хватает девяти десятым сайтов. Для редких расширений удобно смотреть типы в справочнике MIME: вводите расширение или тип и видите пару, а смежные темы кодов ответов и заголовков разобраны в статьях про коды ошибок HTTP и форматирование JSON.

X-префиксы: откуда взялись и куда ушли

В записях вроде application/x-javascript или image/x-icon буквы x- означают experimental, неофициальный подтип, который не прошёл регистрацию в IANA. Раньше так поступали со всеми форматами, до которых официальный реестр не дотянулся, и половина веба жила на x-именах. В 2012 году RFC 6648 отказался от префикса для новых регистраций: типы регистрируют сразу под чистым именем, а старые x-записи остаются в ходу ради совместимости с историческими системами.

Самый живой памятник той эпохи это application/x-www-form-urlencoded: тип по умолчанию для HTML-форм до сих пор носит x-префикс, хотя давно стал стандартом де-факто. При работе с XML и смежными форматами полезно держать под рукой форматтер XML: MIME-тип application/xml и text/xml означают одно содержимое с разными историческими метками, а разбираться в различиях двух форматов данных проще по статье про XML и JSON.

Multipart и загрузка файлов

Форма с файлами не может передаваться обычным urlencoded-типом: бинарные данные в текстовой кодировке искажаются. Для этого случая существует multipart/form-data: тело запроса делится на части, границей служит случайная строка boundary из заголовка Content-Type, и каждая часть несёт собственные заголовки с именем поля и своим типом содержимого. Границу составляет браузер, поэтому вручную её собирать не нужно, но увидеть устройство полезно хотя бы раз.

Тело multipart/form-data
Content-Type: multipart/form-data; boundary=----X9aQ

------X9aQ
Content-Disposition: form-data; name="title"

Отчёт за ноябрь
------X9aQ
Content-Disposition: form-data; name="file"; filename="doc.pdf"
Content-Type: application/pdf

%PDF-1.7 ...
------X9aQ--

Второй multipart-формат, multipart/byteranges, сервер использует при ответах на запросы диапазонов: видео и большие файлы качаются кусками, и каждая часть помечена своим диапазоном байтов. Устройство то же: общий boundary, части со своими заголовками.

Как проверить MIME своего файла и ответа

  1. в DevTools откройте вкладку Network, перезагрузите страницу и выберите интересующий файл;
  2. в блоке Response Headers найдите строку content-type, это то, что реально отдал сервер;
  3. для проверки без браузера выполните curl -I с адресом файла, заголовки придут первыми;
  4. для локального файла на Linux и macOS подойдёт команда file, она определяет формат по сигнатуре, а не по расширению.
Проверка ответа сервера через curl
curl -I https://example.com/logo.svg

HTTP/1.1 200 OK
Content-Type: application/octet-stream

Вывод: тип неверный, картинка в img не отрисуется.
Исправление на своём хостинге, .htaccess:
AddType image/svg+xml .svg

Бывает и так, что тип теряется по пути: фронтенд ждёт application/json, а прокси на маршруте возвращает свою HTML-страницу ошибки с типом text/html. Поэтому при разборе инцидентов с API заголовки смотрят на каждом звене: клиент, прокси, сервер. Часто одного взгляда на Network-панель хватает, чтобы звено, подменившее ответ, нашлось по содержимому и типу одновременно.

Если сервер ваш, исправляется всё в конфигурации: в Apache это директива AddType, и полный разбор есть в статье про .htaccess. На чужом хостинге остаётся писать в поддержку, цитируя вывод curl, такие обращения решаются быстро, потому что проблема воспроизводится одной командой.

Типичные ошибки хостингов

  • .svg отдаётся как application/octet-stream, классика старых конфигураций, картинки не видны;
  • .woff2 не описан вовсе, шрифты грузятся только при выключенном nosniff;
  • .json отдаётся как text/html, клиентские приложения падают на парсинге;
  • .webp неизвестен старым серверам, картинки заменяются на png даже при наличии webp-версий;
  • для text-типов не выставляется charset, и страницы с русским текстом отображаются в неверной кодировке.

Типичные ошибки

  • путают Content-Type запроса и ответа: клиент отправляет application/json в запросе, а сервер отвечает text/html, и сторона виновника не очевидна без просмотра заголовков;
  • ставят text/html для API-ответов, чтобы «браузер показал»: показывать научится и application/json, а вот инструменты сломаются;
  • определяют тип только по расширению: переименованный файл сохраняет чужой тип, верить стоит содержимому;
  • забывают charset у text-типов и получают кракозябры на странице;
  • дублируют boundary при ручной сборке multipart-запросов, и сервер обрезает тело на первой же части;
  • ставят nosniff, не проверив типы всех ресурсов: защита включается, а половина сайта перестаёт применяться.

Частые вопросы

Чем application/json отличается от text/javascript?

Ничем в содержимом и всем в договорённости: JSON это синтаксис данных, а не исполняемый код. text/javascript обещает браузеру, что файл можно исполнить как скрипт, application/json обещает инструментам, что это данные для разбора. Файл с JSON допустимо отдать и как application/octet-stream с последующим разбором на клиенте, но заголовок application/json снимает все вопросы.

Обязателен ли charset=utf-8 в заголовке?

Формально только для text-типов, и то не всегда. Практически указывайте его всегда для text/html и text/css: без параметра браузер угадывает кодировку, и на страницах с кириллицей угадывание регулярно проигрывает. В JSON по стандарту кодировка всегда UTF-8, параметр там избыточен.

Что браузер делает с неизвестным типом?

По умолчанию предлагает сохранить файл на диск: application/octet-stream и незарегистрированные типы трактуются как «скачай, это не для показа». Именно поэтому неверно настроенный тип так заметен: контент не ломается, а исчезает со страницы и уходит в загрузки.

Кто утверждает MIME-типы?

Реестр ведёт IANA по процедуре регистрации медиа-типов. Новые форматы проходят заявку, и после регистрации имя становится официальным. Для практики это значит, что выдумывать типы не нужно: для нестандартного содержимого существует нейтральный application/octet-stream, а для большинства форматов официальное имя давно найдено.

Почему у .js встречаются три разных типа?

Исторически: application/x-javascript жило до регистрации, application/javascript стало первой официальной записью, а RFC 9239 в 2022 году утвердил text/javascript как предпочтительный. Браузеры принимают все три, так что старые конфигурации продолжают работать, но новые разнарядки стоит писать по современному стандарту.

При разборе «почему не работает» начинайте с вкладки Network: сравните content-type ответа с ожидаемым типом ресурса. Одна строчка в заголовках объясняет половину загадок про невидимые картинки, неприменённые стили и падающий парсинг, и проверяется она быстрее, чем гипотезы про кэш.

Читайте также