Command Palette

Search for a command to run...

Hectal
PHASE 11Intermediate ~37 min· topic 1 of 6

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.files or clipboardData.files. It looks like an array but has no map.
Media type (MIME type)
A label such as image/png or application/pdf that 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 file with a mediaType, an optional filename and a url.
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.

One door for three entrancesdiagram
Rendering diagram…

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.

composer.jsxwhole filejsx
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. 1

    Raise the limit

    In the first example change maxBytes: 5 * MB to maxBytes: 8 * MB and 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. 2

    Skip preventDefault on dragover

    In the second example delete the dragover line that calls preventDefault(). The first console line now says dragover 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() on dragover the page is not a valid drop target, and the browser opens the dropped file in the tab instead.

  3. 3

    Add a file count limit

    In addFiles of 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. 4

    Preview, then revoke

    In a scratch example, make a PNG with the pngFile helper from the first example, call URL.createObjectURL(file) and set it as the src of an <img>. The picture loads (20x20 here). Now call URL.revokeObjectURL(url): the picture already on the page stays, but fetch(url) fails with a TypeError, because the address is gone.

    Try the same with new File(["hello"], "notes.png", { type: "image/png" }). The <img> fires error and never loads, so a preview needs an onerror that shows the file name instead of a broken-image icon.

Code & diagrams

Check a file before you accept it

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

Press Run to see the page.

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 unknown
Three ways in: picker, drop and paste

A 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

Press Run to see the page.

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.png
Send files with the message

The (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

Press Run to see the page.

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.
When an upload fails, keep the other files

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

Press Run to see the page.

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=done

Break 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.

terminal
$ Console, first and last lines
── what you'll see ──
receipt.png -> ok
...
photo.png -> ok

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 accept attribute 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.status first and do not call res.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. 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. 2

    A File is a browser object with a name, a size in bytes, a type (the media type, such as image/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 with new File([bytes], "receipt.png", { type: "image/png" }), which is how every example here works without a real picker.

  3. 3

    Three ways in. (1) The picker: <input type="file"> fires a change event and input.files holds a FileList. (2) Drag and drop: the drop event carries event.dataTransfer.files, but the page must call preventDefault() on dragover too, or the browser never allows the drop and opens the file in the tab instead. (3) Paste: the paste event carries event.clipboardData.files, which is how a screenshot copied to the clipboard arrives. All three give you the same thing, a list of File objects, so send them through one function.

  4. 4

    Check in this order, cheapest first. Empty (size === 0), too big (size over your limit), type not allowed (type not in your list), and finally what the bytes really say. The type and the name come from the user's computer and are only hints: renaming notes.txt to notes.png changes the name, and in Chromium the type is guessed from the extension. Reading the first few bytes (the magic number: a PNG starts with 137 80 78 71, a JPEG with 255 216 255, a PDF with the text %PDF) tells you what the file really is.

  5. 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. 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 }, where url is 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 a FileList straight from the input and converts it for you with convertFileListToFileUIParts. Topic 11.2 explains why a data URL is bigger than the file.

  7. 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 with URL.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

01

What are the three ways a file reaches a web page, and what must you do to make drag and drop work?

02

Why should you check a file's first bytes and not only file.type?

03

Where should file limits be enforced, and why in more than one place?

04

What does sendMessage({ text, files }) do with a FileList?

05

An upload of four files fails for one of them. What should the screen do?

Practice

01

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.

02

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.

03

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.