Image MIME Type: Definition, Magic Bytes and Errors

An image MIME type such as image/png labels what a file is. See the magic bytes behind each type, a measured test of wrong headers in Chrome, and code.

By Simon Chiu · Updated

An image MIME type (also called a media type or content type) is a standard label such as image/png or image/webp that tells a browser, crawler or program what format an image file is, usually sent in the HTTP Content-Type header.

The label has a type and a subtype separated by a slash. IANA keeps the official list. This entry is part of the screenshot and rendering glossary.

A plain explanation

A file name like shot.png is only a hint. On the web, the server's Content-Type header is the label that counts, and the file's first bytes are the truth. Browsers, link preview crawlers and image libraries use one or both to decide how to decode the file. Problems start when the three disagree: a WebP saved as .png, or a server that sends every file as application/octet-stream.

Format MIME type First bytes (hex) As text
PNG image/png 89 50 4E 47 0D 0A 1A 0A .PNG....
JPEG image/jpeg FF D8 FF
WebP image/webp 52 49 46 46 .. .. .. .. 57 45 42 50 RIFF....WEBP
AVIF image/avif bytes 4 to 11: 66 74 79 70 61 76 69 66 ftypavif
GIF image/gif 47 49 46 38 39 61 GIF89a
PDF application/pdf 25 50 44 46 2D %PDF-

We read the PNG, JPEG, WebP and PDF rows from files that Chrome 155 produced through Puppeteer on 2026-10-15. Chrome's JPEG started FF D8 FF E0, the JFIF variant. AVIF and GIF come from their specifications.

What a wrong header does in Chrome

We served the same PNG with six Content-Type values, each with and without X-Content-Type-Options: nosniff, and loaded all twelve in <img> tags from a page on another origin (localhost loading from 127.0.0.1).

A grid of twelve image slots, each labelled with the Content-Type the server sent. Ten show the same dark blue test card. Two are empty and striped: text/plain with nosniff, and text/html with nosniff.
One PNG, twelve header combinations, loaded cross-origin in Chrome 155 through Puppeteer 25.14.0. Striped slots failed to load. 2026-10-15.
Content-Type sent Without nosniff With nosniff
image/png (correct) Renders Renders
image/jpeg (wrong image type) Renders Renders
application/octet-stream Renders Renders
text/plain Renders Blocked
text/html Renders Blocked
no header Renders Renders

When the page and the images had the same origin, all twelve rendered. We also opened the image URLs directly as pages: with image/png and text/plain, Chrome showed the image, but with application/octet-stream it started a download and the navigation was aborted.

So inside an <img>, Chrome forgives a lot by reading the bytes. The header matters more elsewhere: for direct links, for cross-origin loads with nosniff, for CDNs that cache by type, and for link preview crawlers, which may reject an og:image whose type is not an image.

How it works technically

For an image request, Chrome looks at the declared type and at the first bytes. If the bytes match a known image signature, it decodes them, even when the header names another image type. X-Content-Type-Options: nosniff tells the browser not to second-guess the header. For cross-origin responses, Chrome's Opaque Response Blocking also refuses responses that are labelled as HTML, plain text or JSON when nosniff is set, because such responses could leak private data into another site's page. That combination is exactly what failed in our test.

How to set it in Puppeteer, Playwright and RenderScreenshot

The capture tools do not send a MIME type; they write bytes. Your job is to make the name, the header and the bytes agree.

Puppeteer. The type option chooses the encoder. Name the file after it.

import puppeteer from 'puppeteer';
import { writeFile } from 'node:fs/promises';

const url = process.argv[2] ?? 'https://example.com';
// Puppeteer picks the encoder from `type`, not from the file name.
const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.goto(url);
  const shot = await page.screenshot({ type: 'webp', quality: 80 });
  await writeFile('shot.webp', shot); // name it after the real type
  console.log('first bytes:', Buffer.from(shot).toString('latin1', 0, 4), Buffer.from(shot).toString('latin1', 8, 12));
} finally {
  await browser.close();
}

Playwright. Log the Content-Type of every image a page loads, which finds servers that mislabel files.

import { chromium } from 'playwright';

const url = process.argv[2] ?? 'https://example.com';
const browser = await chromium.launch();
try {
  const page = await browser.newPage();
  // Record the Content-Type of every image the page loads.
  page.on('response', (res) => {
    if (res.request().resourceType() === 'image') {
      console.log(res.status(), res.headers()['content-type'] ?? '(none)', res.url().slice(0, 80));
    }
  });
  await page.goto(url, { waitUntil: 'networkidle' });
  // Set type explicitly so the bytes always match the file name.
  await page.screenshot({ path: 'shot.jpg', type: 'jpeg', quality: 80 });
  console.log('wrote shot.jpg');
} finally {
  await browser.close();
}

Check the bytes before you serve them. Use this when files come from users or from other systems.

// Identify an image by its first bytes, whatever its extension or header says.
import { open } from 'node:fs/promises';

const SIGNATURES = [
  { mime: 'image/png', test: (b) => b.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) },
  { mime: 'image/jpeg', test: (b) => b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff },
  { mime: 'image/webp', test: (b) => b.toString('latin1', 0, 4) === 'RIFF' && b.toString('latin1', 8, 12) === 'WEBP' },
  { mime: 'image/gif', test: (b) => ['GIF87a', 'GIF89a'].includes(b.toString('latin1', 0, 6)) },
  { mime: 'image/avif', test: (b) => b.toString('latin1', 4, 8) === 'ftyp' && ['avif', 'avis'].includes(b.toString('latin1', 8, 12)) },
  { mime: 'application/pdf', test: (b) => b.toString('latin1', 0, 5) === '%PDF-' },
];

export async function sniff(path) {
  const fh = await open(path);
  try {
    const { buffer } = await fh.read(Buffer.alloc(16), 0, 16, 0);
    return SIGNATURES.find((s) => s.test(buffer))?.mime ?? 'application/octet-stream';
  } finally {
    await fh.close();
  }
}

for (const path of process.argv.slice(2)) console.log(path, await sniff(path));

RenderScreenshot. The response Content-Type follows output.format: image/png (the default), image/jpeg, image/webp or application/pdf. Check it with curl -D -:

curl -sS -D - -o shot.webp \
  "https://api.renderscreenshot.com/v1/screenshot?url=https://example.com&format=webp" \
  -H "Authorization: Bearer $RS_API_KEY" | grep -i '^content-type'

If you store the file and serve it yourself, copy that header, or derive it from the format you requested. See response formats and response headers.

Tested on 2026-10-15 with Node 22.18.0, Puppeteer 25.14.0 (Chrome 155) and Playwright 1.64.1 (Chromium 156) against https://example.com and a local test server; the sniffer identified a WebP, a JPEG, a PNG and a PDF correctly. The RenderScreenshot request follows the API docs and was not run for this page.

Common mistakes

  • image/jpg. It is not a registered type. Use image/jpeg.
  • One type for every file. Object stores and CDNs often default to application/octet-stream. Set the type on upload.
  • Converting without renaming. A PNG converted to WebP and saved as .png gets served as image/png by most static servers.
  • Sending a download header for inline images. Content-Disposition attachment makes direct links download instead of display.
  • WebP and AVIF: newer formats whose types older tools may not know.
  • Content-Disposition: the header that decides inline or download.
  • MHTML: an archive in which every part carries its own MIME type.

To get images with the right type every time, get a free API key.

Frequently asked questions

What is the MIME type for a WebP image?
image/webp. PNG is image/png, JPEG is image/jpeg (not image/jpg), AVIF is image/avif, GIF is image/gif and SVG is image/svg+xml.
Does a wrong Content-Type break an image?
Often not on your own site, because browsers sniff the bytes. In our cross-origin test, Chrome blocked the image only when the type was text/plain or text/html and the server also sent X-Content-Type-Options nosniff.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.