Topic 11.1
File Attachments
In one line
A file reaches your page through a picker, a drop or a paste. Check its size, type and contents before you accept it, show a preview, send it as a file part of the message, and handle an upload that fails without losing the other files.
Think of it like this
Posting a parcel at a counter. The clerk looks at the box before taking it: is it too heavy, is it empty, is it something we cannot send? Only then does it go on the shelf with a label. A file attachment needs the same counter: look first, label it, and tell the person at once if the answer is no.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- File
- A browser object for a user's file: a name, a size in bytes, a media type and the bytes. It has no disk path.
- Blob
- A chunk of raw bytes with a media type. A File is a Blob that also has a name.
- FileList
- The list of files in
input.files,dataTransfer.filesorclipboardData.files. It looks like an array but has nomap. - Media type (MIME type)
- A label such as
image/pngorapplication/pdfthat says what kind of data a file holds. - Magic number
- The first bytes of a file, which identify its real format regardless of its name.
- DataTransfer
- The object that carries dragged or pasted data, including files.
- File part
- A message part of type
filewith amediaType, an optionalfilenameand aurl. - Data URL
- An address that contains the data itself, written as
data:<type>;base64,<text>. - 413 Payload Too Large
- The status a server returns when the request body is bigger than it accepts.
- Object URL
- A temporary address for a Blob made with
URL.createObjectURL, used for previews.
Step by step
01One door for three entrances
The picker, the drop zone and paste all end in the same function, addFiles(list). That function is where every rule lives. Write the rules once, and a new entrance (a camera button, say) costs two lines.
02Check before you accept
The first live example runs six files through the checks: a real PNG, a six-megabyte PDF, an empty file, an executable, a file with no type, and a text file that claims to be a PNG. Each gets one verdict. Read the order: the cheap checks use only size and type, and only the last one reads bytes.
The "lying" file passes every check on name and type and is caught only by the magic number. That is why sniffing exists.
03Let the browser know you accept drops
The second example fires real change, dragover, drop and paste events at the page, carrying files built in code. Watch two details in the output. The page calls preventDefault() on dragover and drop, so the browser does not navigate to the file. And a paste with only text is left alone, so normal pasting keeps working.
After handling a picker choice the code sets input.value = "". Without that, choosing the same file twice in a row fires no second change, and the user thinks the button is broken.
04Send them with the message
In the third example the form passes input.files to sendMessage({ text, files }). The server route logs the parts it received, so you can see the message really carries a file part. The stand-in model cannot see the image: its reply is scripted to count the files and say so.
const { messages, sendMessage, status } = useChat({ transport: new DefaultChatTransport({ api: "/api/chat" }) });
function submit(e) {
e.preventDefault();
sendMessage({ text: prompt, files: inputRef.current.files }); // a FileList, converted for you
inputRef.current.value = "";
}05When an upload fails
The fourth example uploads three files to a separate route. One is too big (413), one is the wrong type (415), one works. The page keeps a status for each file (queued, uploading, failed, done), shows the reason next to the failed one, and a retry sends only the files that failed. A failure for one file never throws away the others.
In production the cause also includes a dropped connection and a server that restarts mid-upload. The retry code is the same: the file stays in the list until it is done.
Try it yourself
- 1
Raise the limit
In the first example change
maxBytes: 5 * MBtomaxBytes: 8 * MBand run it.The six-megabyte PDF now gets past the size check. It is then judged by its type and by its bytes: its bytes are all zero, so the magic-number check calls it out. A larger limit lets more through; it does not make the file valid.
- 2
Skip preventDefault on dragover
In the second example delete the
dragoverline that callspreventDefault(). The first console line now saysdragover default prevented: false, and the drop is still logged, because the example fires the events itself.In a real browser the drop would never reach your handler: without
preventDefault()ondragoverthe page is not a valid drop target, and the browser opens the dropped file in the tab instead. - 3
Add a file count limit
In
addFilesof the second example, stop at 3 files in total and log "too many files" for the rest.Providers also limit how many files one message can carry, so a count limit belongs next to the size limit.
- 4
Preview, then revoke
In a scratch example, make a PNG with the
pngFilehelper from the first example, callURL.createObjectURL(file)and set it as thesrcof an<img>. The picture loads (20x20 here). Now callURL.revokeObjectURL(url): the picture already on the page stays, butfetch(url)fails with aTypeError, because the address is gone.Try the same with
new File(["hello"], "notes.png", { type: "image/png" }). The<img>fireserrorand never loads, so a preview needs anonerrorthat shows the file name instead of a broken-image icon.
Code & diagrams
Files are made in code with new File([bytes], name, { type }), because the preview has no file picker. The checks run cheapest first, and only the last one reads bytes.
Preview
Console
Nothing logged yet.Expected console output
receipt.png -> ok (png)
scan.pdf -> rejected: over 5 MB
empty.png -> rejected: the file is empty
setup.exe -> rejected: application/x-msdownload is not allowed
README -> rejected: the browser could not tell the type
photo.png -> rejected: says png but the bytes say unknownA real user does these with a mouse. The example builds the same events with files made in code, so you can read exactly what each handler receives.
Preview
Console
Nothing logged yet.Expected console output
dragover default prevented: true
picker: 1 file(s): menu.pdf
picker reset, files left in the input: 0
drop: 2 file(s): a.png, b.png
drop default prevented: true
drop: no files (it was text), ignored
paste: 1 file(s): screenshot.png
paste default prevented: true
paste: text only, left to the browser
text paste default prevented: false
all files: menu.pdf, a.png, b.png, screenshot.pngThe (server) lines are the route's own log. The second send has no file, and the model is told so. The scripted visitor sends again when the first answer is complete.
Preview
Console
Nothing logged yet.Expected console output
(server) parts: file receipt.png image/png, text
(server) parts: text
user: [receipt.png] What is on this? | assistant: I received 1 file(s). This stand-in cannot look inside them. | user: What is on this? | assistant: No files came with that message.Four files go up together. A failure for one file is stored on that file, not thrown, so the others finish. The retry sends only what failed. The (server) line is the route's own log of the crash.
Preview
Console
Nothing logged yet.Expected console output
(server) Error: storage unavailable
big.png=failed notes.txt=failed flaky.png=failed ok.png=done
show: big.png is too large. Choose a smaller file.
show: notes.txt is not a PNG. Choose a PNG image.
show: flaky.png could not be saved. Try again.
retry only the failed ones
big.png=failed notes.txt=failed flaky.png=done ok.png=doneBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Trust the file type
In the first example, replace the four lines at the end of check (from const real = await sniff(file); down to return "ok (" + real + ")";) with the single line return "ok";. Run it.
Myth vs fact
Myth
file.type tells you what the file is.
Fact
It is a guess from the extension, supplied by the user's computer. It can be empty or wrong. Only the bytes say what a file is.
Myth
A size check in the browser is enough.
Fact
It gives a fast message but anyone can skip it. The server must check size and type again, and the host enforces its own body limit.
Myth
Drag and drop works as soon as you add a drop handler.
Fact
The page must also cancel dragover. Without that the drop never fires and the browser navigates to the file.
Myth
Choosing a file twice fires change twice.
Fact
Choosing the same file again fires nothing unless you reset input.value.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Revoke object URLs. Every
URL.createObjectURL(file)keeps the file in memory until it is revoked or the page closes. Create the preview in an effect and revoke it in the cleanup function, or a user who attaches thirty photos in one session leaks all of them. - ▸
Set the
acceptattribute on the input (accept="image/png,image/jpeg,application/pdf") so the picker hides other files, but treat it as a convenience: drop and paste ignore it, and users can switch the picker to "All files". - ▸
A request body with files can be rejected before your code runs. Hosting platforms set a maximum body size for functions, and a bigger request gets a 413 from the platform, possibly with an HTML page instead of your JSON. Check
res.statusfirst and do not callres.json()blindly on a failure. - ▸
Strip metadata you do not need. Photos carry GPS position and camera details inside the file. If you resize and re-encode on a canvas (Topic 11.2), that metadata is dropped as a side effect.
Remember this
- 1
The problem this solves: A file is the first input in this course that the user does not type. It can be huge, empty, the wrong kind, or named "photo.png" while being a text file. If your page sends it blindly, the person waits for an upload that was always going to fail, and you pay for the bytes. This topic builds the counter: three ways in, one set of checks, a preview, and honest failure messages.
- 2
A File is a browser object with a
name, asizein bytes, atype(the media type, such asimage/png) and the bytes themselves. It is a kind of Blob, a chunk of raw data. You never get a path on the user's disk, only the object. You can make Files yourself in code withnew File([bytes], "receipt.png", { type: "image/png" }), which is how every example here works without a real picker. - 3
Three ways in. (1) The picker:
<input type="file">fires achangeevent andinput.filesholds aFileList. (2) Drag and drop: thedropevent carriesevent.dataTransfer.files, but the page must callpreventDefault()ondragovertoo, or the browser never allows the drop and opens the file in the tab instead. (3) Paste: thepasteevent carriesevent.clipboardData.files, which is how a screenshot copied to the clipboard arrives. All three give you the same thing, a list ofFileobjects, so send them through one function. - 4
Check in this order, cheapest first. Empty (
size === 0), too big (sizeover your limit), type not allowed (typenot in your list), and finally what the bytes really say. Thetypeand the name come from the user's computer and are only hints: renamingnotes.txttonotes.pngchanges the name, and in Chromium the type is guessed from the extension. Reading the first few bytes (the magic number: a PNG starts with137 80 78 71, a JPEG with255 216 255, a PDF with the text%PDF) tells you what the file really is. - 5
Why limits exist. Every file you send is part of the request, and the model provider limits request size and charges for what it reads. Your server and your host have a maximum request body too: when a request is bigger the answer is 413 Payload Too Large, often before your code runs. Set your own limit lower than theirs, check it in the browser for a fast, friendly message, and check it again on the server, because the browser check can be bypassed.
- 6
Send it as a file part. In the AI SDK a message is a list of parts. A file is
{ type: "file", mediaType, filename, url }, whereurlis a normal link or a data URL (the file's bytes written as text inside the address:data:image/png;base64,iVBOR…).sendMessage({ text, files })accepts aFileListstraight from the input and converts it for you withconvertFileListToFileUIParts. Topic 11.2 explains why a data URL is bigger than the file. - 7
Show what you have. Before sending, list each file with its name, size and a remove button, and show a thumbnail for images with
URL.createObjectURL(file). Release it withURL.revokeObjectURL(url)when the thumbnail goes away, or the browser keeps the file in memory until the tab closes. After sending, keep the files visible in the message so the conversation still makes sense.
Explain it without notes
What are the three ways a file reaches a web page, and what must you do to make drag and drop work?
Why should you check a file's first bytes and not only file.type?
Where should file limits be enforced, and why in more than one place?
What does sendMessage({ text, files }) do with a FileList?
An upload of four files fails for one of them. What should the screen do?
Practice
Extend check in the first example so a file named with an uppercase extension (PHOTO.PNG) is judged by its type, not its name, and add a rule that file names longer than 80 characters are shortened for display.
Write describeSize(bytes) that returns "512 B", "1.5 KB" or "2.3 MB" for use in the file list, and check it with 0, 1023, 1024 and 5 million bytes.
In the second example, make the zone ignore a drop that mixes an allowed PNG and a disallowed .exe: accept the PNG, and log a message for the .exe.
Trade-offs
- ↔
Sending the file inside the chat request (a data URL) is simplest, and it works for a few small images. Uploading to storage first and sending only a link keeps requests small and lets you reuse the file, at the price of a second route, storage and clean-up (Topic 11.2).
- ↔
Strict checks stop waste and abuse but reject files that would have worked. Err towards allowing the formats your provider documents, and say clearly which ones those are.
Done when you can
I can accept files from a picker, a drop and a paste through one function.
I can reject empty, oversized and wrong-type files with a message a person can act on.
I can tell a file's real format from its first bytes.
I can send files to the model as file parts with
sendMessage.I can show a preview and release it with
revokeObjectURL.I can handle a failed upload without losing the other files.