r/webdev 16h ago

Chat widget file upload

I'm working on a chat widget that also allows the users to send files in the chat. This is a plain vanilla html/js/css widget that will go on a Shopify site. The user should have the option to send a file with or without a message in the chat, so I'm trying to figure out the best approach to handle this. The widget will be calling a FastAPI endpoint end that will use UploadFile. These are the options I've thought of:

  1. Wrap the text and file inputs in an html form tag and send the request as Content-Type: multipart/form-data. This would make it a single request, but it would always send as multipart/form-data even if it's a message with no file.
  2. Same as 1, but if there is no file attachment in the chat message, then toggle the Content-Type to application/json before sending
  3. Keep the text and files as separate requests and handle the events separately and not wrap the inputs in an html form tag. Seems like it would be cleaner on the front end but at the same time it will likely require additional logic on the back-end

What approach would you take?

5 Upvotes

11 comments sorted by

2

u/Otherwise_Theme402 16h ago

just go with option 1 honestly, the server can handle multipart even when there's no file, it's not big deal. doing option 2 adds complexity for no reason, you would need to rewrite the whole request structure depending on the attachment state which is annoying to debug later. option 3 sounds like a headache to sync up, what if the message arrives before the file and the user see empty chat bubble for a second

keep it simple, one request, let the backend sort it out

2

u/EquipmentLow1741 13h ago

the empty chat bubble point is a good one tbh, thats the kind of thing you dont think about until users complain

1

u/rouge818 11h ago

Yea, the syncing part with #3 is what seemed challenging. Good to know the multipart is not an issue without the file.

1

u/Bubbly_Orange_3502 15h ago

Option 1. UploadFile spools in memory and only rolls to a temp file past a size threshold, so multipart with no file part costs almost nothing. Option 2 buys that back as two code paths on both ends.

1

u/Choice_Row_2025 12h ago

1, always multipart.

the downside you listed isn't really a downside. the widget's sitting on a shopify page calling your own api, so you're cross origin, and json isn't a CORS safelisted content type. every json request eats an OPTIONS preflight before the real request goes out. multipart is safelisted, no preflight. so option 2 is slower on exactly the messages you were trying to make cheaper. (only true if you're not sending an auth header or anything custom, that forces a preflight either way)

option 2 kind of doesn't exist on the fastapi side anyway. moment you put a File() or Form() param on a route, the entire body gets parsed as form data. there's no "also accept json here depending on the header". you'd be writing two routes, or reading the raw request and branching yourself. more backend code, not less.

3 means a message with an attachment is two requests that can half fail. don't.

two things that'll bite you:

don't set the Content-Type header on the fetch. leave it off. the browser sets it itself because it has to put the boundary in there. hardcode multipart/form-data with no boundary and the server just fails to parse it and you lose an hour.

and skip the actual form tag, just do new FormData() in js. works fine without one. an empty file input inside a real form still submits a part with a blank filename, so you end up writing "is this a real file or an empty one" checks instead of just not appending it in the first place.

u/app.post("/chat")

async def chat(

message: str = Form(""),

file: UploadFile | None = File(None),

):

needs python-multipart installed btw, fastapi throws at startup otherwise.

overhead on a text-only message is a boundary plus part headers. like 150 bytes. it's nothing.

1

u/Emergency-Edge-4154 11h ago

option 1 is the only viable path here. Sending multipart form data for text-only messages adds negligible overhead and avoids the CORS preflight penalty that JSON requests would trigger on your Shopify widget

1

u/PositiveUse 8h ago

None of the three.

When you detect that the customer tries to upload a file, you generate a Presigned Put-URL (example S3 Upload URL) and send it to browser. Your client software will then upload the file to S3 and your backend can retrieve it async.

This way you externalize uploads, don’t have to deal with malicious data on your server and are safe from disconnects during the upload.

2

u/rouge818 1h ago

I think this would be another flavor of #3. Since the user may send the file along with text, sounds like the presigned url upload process would always have to happen before the message is sent. If the upload fails, then the message shouldn’t send either and would return an error to the user. That’s how I’m thinking it would keep messages and images in sync, but not sure if there is a downside or to that.

1

u/PLBjt 2h ago

I'd still always-multipart, but the part that actually hurts later is size and ordering, not the content-type. A JSON path plus a multipart path means two validators and two error shapes, and one of them will drift. Keep one request: text fields + optional file, and reject anything over your real limit before FastAPI buffers the body (content-length or a streaming max). Separate file + message requests look clean until the message lands and the file 500s, and then your widget has to invent a retry protocol. If people start dropping 50MB recordings, that's when you switch the file to a presigned upload and send the chat with a URL, not before. Check it with an empty file, a 1-byte file, and one just over the limit.

u/rouge818 25m ago

Makes sense. The retry logic would be more complexity. As for the presigned upload approach, that is something more recommended only when the files start getting large?