How to Find a File’s MIME Type: Extension, Header and Content Detection
There are four places to get a file's MIME type, and they answer different questions. The extension says what the file is supposed to be, a server's `Content-Type` header says what the server claims, the browser's `File.type` is the operating system's guess from the name, and the first bytes show what the file most likely is. Which one you need depends on one thing: whether you trust the file.
Look up the extension when you need the conventional type
Extension lookup is the right tool when you already trust the file: configuring nginx, setting headers for your own build output, or writing a download handler for files you generated. .pdf maps to application/pdf, .png to image/png, and .json to application/json.
Extension MIME type
.html text/html
.css text/css
.js text/javascript
.json application/json
.svg image/svg+xml
.pdf application/pdf
A lookup never opens the file. Rename payload.html to photo.jpg and every extension table will call it image/jpeg. Mappings also differ between machines: Python's mimetypes adds the operating system's own tables to its built-in list, so the same call can give different answers on your laptop and on the server.
To get the extension, take the part of the file name after the last dot, lowercase it and drop the dot, but only after stripping any path and query string: /files/report.PDF?v=2 should give pdf, not pdf?v=2.
Read the HTTP Content-Type for remote resources
For a URL, ask the server. curl -I sends a HEAD request, and -L follows redirects to the final file, which matters for download links that bounce through a CDN or a signed URL. If a server answers HEAD badly, with a 405 or different headers, make a real GET and discard the body with curl -s -D - -o /dev/null URL.
curl -I -L https://example.com/manual.pdf
HTTP/2 200
content-type: application/pdf
content-length: 482193
The header is the server's opinion, not detection. A misconfigured origin labels every unknown extension with a fallback: nginx's stock configuration sets default_type application/octet-stream, and other servers fall back to text/plain. application/octet-stream just means arbitrary bytes, usually triggers a download, and tells you nothing about the real format.
Parameters follow a semicolon: text/html; charset=utf-8 is the type text/html plus a charset. Keep them when you send the header, but strip them before comparing against an allow-list, or text/csv; charset=utf-8 fails a check for text/csv.
Inspect local bytes with file and libmagic
On Linux and macOS, the file command reads the start of a file and matches it against the libmagic database. In scripts, ask for the MIME type alone with --mime-type --brief, so you get one clean token back instead of a description.
file --mime-type --brief report.pdf
# application/pdf
file --mime --brief data.csv
# text/csv; charset=us-ascii
It is a heuristic. Text formats blur into each other, a file of a few bytes gives libmagic little to go on, polyglot files satisfy more than one format on purpose, and results can change between versions of the magic database, so two servers may label the same file differently.
Treat the result as a strong clue about the format, not as proof that the file is complete or harmless. If your application is going to decode an image or unpack an archive, the real test is doing exactly that with the library you will use, and rejecting the file when it fails.
Detect the MIME type in PHP and Python
In PHP, the Fileinfo extension wraps libmagic. Point it at the temporary file on disk, $_FILES['doc']['tmp_name'], not at the original name or at $_FILES['doc']['type'], which the browser supplied.
$finfo = new finfo(FILEINFO_MIME_TYPE);
$type = $finfo->file('/path/to/upload.tmp');
if ($type === false) {
throw new RuntimeException('Could not inspect file');
}
echo $type; // for example image/png
Python's standard library only does the extension half. mimetypes.guess_type() looks at the name and returns a type plus an encoding for compressed files; it never reads a byte. That is fine for setting headers on your own files and useless for checking uploads, where a libmagic binding such as python-magic does the byte inspection.
import mimetypes
type_, encoding = mimetypes.guess_type('archive.tar.gz')
print(type_) # application/x-tar on common installations
print(encoding) # gzip
# Results come from filename and platform mappings, not file contents.
Name your helpers so the difference cannot be missed. With one function called mime_from_extension() and another called detect_mime_from_bytes(), nobody validates uploads with the wrong one by accident.
The MIME type of a file in JavaScript
In JavaScript, a File from an <input type="file"> has a type property, but the browser fills it from the file name and the operating system's extension mappings, not from the content. Unknown or missing extensions give an empty string. Use it to warn the user early; the server must never rely on it.
input.addEventListener('change', () => {
const file = input.files[0];
console.log(file.name); // photo.png
console.log(file.type); // image/png, or possibly ''
console.log(file.size); // bytes
});
The accept attribute only filters what the file picker shows. A user can switch the picker to all files, drop a file onto the input, or skip the form and post with curl, so the server repeats every size, type and format check.
For a remote file fetched with fetch(), response.headers.get('content-type') returns the server's label. Cross-origin, you only get it if CORS lets the script read the response at all; Content-Type is a CORS-safelisted response header, so it needs no Access-Control-Expose-Headers.
Resolve disagreements between extension and content
When the extension and the bytes disagree, find out why. On your own server it is usually a wrong mapping or a mislabelled export; on an upload it may be an attack. What you must not do is pick whichever signal lets the file through.
name: avatar.jpg
client type: image/jpeg
detected: text/html
result: reject; do not rename or serve inline
name: no-extension
detected: image/png
result: policy decision after successful image decode
When you accept a file, derive its extension and MIME type from what you detected and successfully parsed, and store it under a random name. Keeping the user's invoice.pdf.php as the stored name is how upload directories end up running code.
Remember containers: a DOCX is a ZIP archive, so a detector that answers application/zip is right, just not specific. When the difference matters, look at the entries inside the archive.
Set Content-Type without enabling sniffing
Once you know the type, send it. Browsers choose how to parse and render a response from Content-Type, so take the value from metadata you stored when the file arrived, never from a query parameter. For untrusted files that users should download rather than view, add Content-Disposition: attachment and nosniff:
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="manual.pdf"
X-Content-Type-Options: nosniff
...bytes...
nosniff stops browsers from second-guessing the declared type: a script served as image/png is not run, and a stylesheet served as text/plain is not applied. It cannot fix a wrong header; it only stops the browser from working around one.
So the method follows the question: look up the extension to configure a server, read the header to debug one, inspect the bytes to classify a local file, and parse the whole file before accepting anything from a stranger.
Frequently asked questions
How do I find a file's MIME type?
For a trusted filename, look up its extension. For a local file, inspect bytes with `file --mime-type`, PHP Fileinfo or a libmagic binding. For a remote URL, inspect its final HTTP `Content-Type` header.
Can a MIME type be determined from the extension?
An extension gives the conventional expected type but does not inspect content. It is suitable for known assets and configuration, not proof that an untrusted file actually has that format.
What does application/octet-stream mean?
It is the generic media type for arbitrary binary data. It normally indicates that the sender cannot or will not identify a more specific format, and it should not bypass an upload allow-list.
Why is a file's MIME type empty in JavaScript?
Browser `File.type` depends on local filename and platform mappings, so unknown or extensionless files may return an empty string. The server must inspect the uploaded bytes and enforce its own policy.
Can the Content-Type HTTP header be wrong?
Yes. It is configured by the server and may be generic or mistaken. Compare it with the expected extension and actual bytes when debugging, and correct the origin configuration rather than relying on browser sniffing.
Is detecting magic bytes enough for upload security?
No. Signatures identify many formats but do not prove the entire file is valid, harmless or non-polyglot. Use a narrow allow-list and parse or decode the complete format before accepting sensitive uploads.