Verify web uploads with Magika and a Python API
A file named photo.jpg might contain a PDF. To check an upload’s type, send its bytes to
Magika’s Python API and apply
an allowlist to the returned label. This walkthrough builds a local Flask app that serves both
the upload page and its classification endpoint, with feedback that follows the current selection.
The browser sends the complete file to Python. Classification happens on that server, even when both processes run on your laptop. An accepted type does not establish that a file is valid or free of malware.
Decide what the label can tell you
Extensions and client-supplied MIME types describe what a file claims to be. Content detectors inspect bytes instead, so renaming a file does not change their input. Magika uses a trained model to distinguish binary and text formats; it can still misclassify a file.
The Python API exposes
identify_bytes(), a success flag in result.ok, and the final prediction in result.output.
Use result.output.label for the allowlist. The default HIGH_CONFIDENCE mode can replace a
low-confidence model prediction with a generic label, which this example rejects along with
other types outside the allowlist.
Magika and signature-based detectors answer a format-identification question. Neither that answer nor a high confidence score proves that a decoder will accept the entire file. Magika’s known limitations also exclude reliable polyglot detection: a file may be interpretable as more than one format.
Integrate Magika into a browser application
Use Linux, Python 3.12 with venv and pip, and a current browser. The example pins Magika 0.6.1 and
Flask 3.1.0; installation needs internet access. Once installed, Magika loads its bundled model
locally, so classification does not need a cloud service. The commands below use Bash.
1. Install Magika
Paste this into a terminal in a directory where you want a new magika-upload project. It refuses
to reuse an existing directory and stops if any setup step fails. The parentheses keep your shell
in its original directory.
(
mkdir magika-upload &&
cd magika-upload &&
mkdir static &&
python3.12 -m venv .venv &&
.venv/bin/python -m pip install magika==0.6.1 flask==3.1.0
)
Continue only after installation succeeds. If it fails, inspect the error and retry in a new directory or repair the incomplete environment before proceeding. Do not delete an existing project to rerun setup.
2. Create a minimal verification API (Flask)
Save this as magika-upload/app.py. The model loads once when the application starts. The /
route serves the page you will create next; Flask also serves the static directory automatically.
from flask import Flask, jsonify, request, send_from_directory
from magika import Magika
from werkzeug.exceptions import RequestEntityTooLarge
app = Flask(__name__)
MAX_FILE_BYTES = 5 * 1024 * 1024
app.config['MAX_CONTENT_LENGTH'] = 6 * 1024 * 1024
ALLOWED_TYPES = {'pdf', 'jpeg', 'png'}
magika = Magika()
@app.get('/')
def index():
return send_from_directory(app.root_path, 'index.html')
@app.errorhandler(RequestEntityTooLarge)
def request_too_large(error):
return jsonify(error='Upload is too large'), 413
@app.post('/verify')
def verify_file():
if 'file' not in request.files:
return jsonify(error='No file provided'), 400
content = request.files['file'].read(MAX_FILE_BYTES + 1)
if not content:
return jsonify(error='File is empty'), 400
if len(content) > MAX_FILE_BYTES:
return jsonify(error='Upload is too large'), 413
result = magika.identify_bytes(content)
if not result.ok:
return jsonify(error='File analysis failed'), 500
if result.output.label not in ALLOWED_TYPES:
return jsonify(error='Only PDF, JPEG, and PNG files are allowed'), 415
return jsonify(
file_type=result.output.label,
mime_type=result.output.mime_type,
)
The file limit is 5 MiB. The separate 6 MiB request limit leaves room for multipart headers while
bounding the whole request. As Flask’s upload guide
explains, parsing uploads may use temporary disk storage. This app does not save accepted files
permanently; it reads at most 5 MiB plus one byte into content before classification.
3. Wire up the front-end
Save this as magika-upload/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Classify a file with Magika</title>
<script src="/static/verify.js" defer></script>
</head>
<body>
<h1>Classify a file with Magika</h1>
<p>PDF, JPEG, or PNG, up to 5 MiB. The file is sent to the local Python server.</p>
<form id="uploadForm" action="/verify" method="post" enctype="multipart/form-data">
<label for="fileInput">File to classify</label>
<input id="fileInput" name="file" type="file">
<button type="submit">Classify file</button>
</form>
<p id="result" role="status">Choose a file, then classify it.</p>
</body>
</html>
Save the following as magika-upload/static/verify.js. Changing the selection clears the previous
result immediately. Submitting again supersedes the previous request, so a slow response cannot
replace feedback for newer work.
const form = document.getElementById('uploadForm')
const input = document.getElementById('fileInput')
const result = document.getElementById('result')
const errors = {
400: 'Choose a non-empty file.',
413: 'File exceeds the upload limit.',
415: 'Only PDF, JPEG, and PNG files are allowed.',
}
let requestId = 0
let activeRequest
input.addEventListener('change', () => {
requestId += 1
activeRequest?.abort()
result.textContent = 'Selection changed. Click Classify file to check it.'
})
form.addEventListener('submit', async (event) => {
event.preventDefault()
const id = ++requestId
activeRequest?.abort()
const file = input.files[0]
if (!file) {
result.textContent = 'Choose a file, then classify it.'
return
}
const controller = new AbortController()
activeRequest = controller
const timeout = setTimeout(() => controller.abort(), 30_000)
const data = new FormData()
data.append('file', file)
result.textContent = `Classifying ${file.name}…`
try {
const response = await fetch('/verify', {
method: 'POST',
body: data,
signal: controller.signal,
})
if (id !== requestId) return
if (!response.ok) {
result.textContent = errors[response.status] ??
`Classification failed (HTTP ${response.status}).`
return
}
const analysis = await response.json()
if (id !== requestId) return
result.textContent = `${file.name}: ${analysis.file_type} (${analysis.mime_type}). ` +
'Accepted by the type allowlist.'
} catch {
if (id !== requestId) return
result.textContent = controller.signal.aborted
? 'Request timed out. Click Classify file to retry.'
: 'Could not reach or read the server. Check the terminal, then retry.'
} finally {
clearTimeout(timeout)
}
})
The request ID guards both the response and its asynchronously read JSON body.
AbortController
stops waiting for superseded requests, and a 30-second timer bounds each browser attempt. Aborting
the browser request does not guarantee that Python stops processing bytes it already received.
From the same parent directory used for setup, start the app:
(
cd magika-upload &&
.venv/bin/python -m flask --app app run --host 127.0.0.1 --port 5050
)
Wait for Flask’s startup message, then open http://127.0.0.1:5050/. Open this URL rather than the
HTML file on disk: the page, script, and /verify endpoint must share an origin. Select a file with
File to classify, then click
Classify file. You can click the button again to retry the same
file. Stop the server with Ctrl+C when finished.
4. Compare results to an allowlist
Try a small, valid PNG renamed to photo.txt. You should see
photo.txt: png (image/png). Accepted by the type allowlist.
The filename does not authorize the upload: only the server’s pdf, jpeg, and png labels do.
For JPEG files the label is jpeg, even when the filename ends in .jpg.
Next, try a non-empty text file renamed to photo.png. It should produce
Only PDF, JPEG, and PNG files are allowed.
Also try an empty file and a file just over 5 MiB. These are rejected before Magika runs.
While a request is pending, select a different file: the page should clear the pending result
and wait for you to classify the new selection.
Resolve a failed request
- Installation or model loading fails: read the terminal error first. Confirm that installation
completed in
.venvand that you are launching its Python executable. The page cannot load until application startup succeeds. - The port is occupied: Flask exits with an address-in-use error. Choose another port in the startup command and open that port’s URL. This is a local development server, not a deployment setup.
- The server rejects the request: HTTP 400 means a missing or empty file, 413 means a size limit, and 415 means a label outside the allowlist. Larger requests can cause a connection reset on the development server instead of a readable 413 response.
- The browser cannot read a response: check the server terminal and URL, then retry. HTTP failures, a stopped server, and timeouts do not count as acceptance. The page displays fixed error messages instead of rendering a server error page or stack trace.
Use the result in an upload workflow
If you extend this app to store files, perform the allowlist check and storage on the same received bytes. A successful classification response must not become permission for a later, unchecked upload. The example returns a classification only; it creates no saved upload or reusable approval.
An allowed label can help choose a downstream image decoder, document parser, or moderation service. Those operations still need their own validation and resource limits. Malware scanning is a separate task, and a format prediction should never be displayed as a safety verdict.
For a managed upload workflow, consult the file verification reference. Keep the same distinction in either design: recognizing a format is one decision in handling an upload, not proof that its contents are safe.
