Wednesday, February 24, 2010

How Not to Upload Files to CouchApp

‹prev | My Chain | next›

I am continuing my exploration of couchapp today by trying to upload attachments to CouchDB documents. I use attachments in my recipe database to include meal and recipe pictures, so I played with them extensively last year (both command line and with Ruby code).

I am not too sure if it is even possible to upload directly from a web form—I believe that only way that this might work is if CouchDB supports POSTs of multipart/form-data to existing documents. CouchDB's HTTP Document API does not mention this, so I am not hopeful.

Before coding, I explore a bit. I'm pretty sure that CouchDB's futon admin interface supports attachment uploads. Maybe I can re-use (or even copy) that. After creating a test document, I notice that there is an "Upload Attachment..." link in futon. Clicking that link I get this dialog:

The form sure acts like an old fashioned document upload, but inspecting the HTML I find:

The form is not multipart/form-data. It does use a normal file <input> tag and the button is a <button type="submit"> tag (which submits forms just like an <input type="submit"> tag). That is really weird, I have never seen a file upload without a enc="multipart/form-data" attribute. Aside from that, this seems pretty solid—the form is being POSTed to the current document, which should push a new thing onto a sub-collection under that document. The _attachments attribute seems like a perfectly good place for CouchDB to address that.

So maybe this will work...

First I create my shows/upload.js show function:
function(doc, req) {
// !json templates.upload
// !json templates._header
// !json templates._footer
// !code vendor/couchapp/template.js
// !code vendor/couchapp/path.js

return template(templates.upload, {
dbname: 'eee',
docid : doc._id,
rev: doc._rev,
title: doc.title,
asset_path: assetPath(),
header: template(templates._header, {}),
footer: template(templates._footer, {})
That is very similar to the edit show function that I have used recently, but I take into account that I will need to address the document directly (dbname/docid) in the parameter list.

Next I create the template for this show function:
<!DOCTYPE html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Recipe Image Upload</title>
<link rel="stylesheet" href="<%= asset_path %>/style/main.css" type="text/css" />

<%= header %>

<h1>Upload to <%= title %></h1>

<form id="recipe-upload" action="/<%= dbname %>/<%= docid %>" method="post">

File to attach:
<input type="file" name="_attachments">

<button type="submit">Upload</button>

<input type="hidden" name="_rev" value="<%= rev %>">

<%= footer %>
No magic in there at all—just like the futon upload I am not specifying multipart/form-data. I am posting directly to the document with only the current document revision number and the uploaded file. Hopefully that will do the trick.

Unfortunately, when I upload I get this:
{"error":"bad_content_type","reason":"Invalid Content-Type header for form upload"}
Dang it.

After some digging, I realize that the "Upload Attachment..." link in futon is not only displaying the form, but also attaching some event listeners. It looks as though some ajaxSubmit() fun is in order. I will give that a whirl tomorrow.

Day #24


  1. I've just been taking a look at this myself, and the above would have worked had you specified the enctype="multipart/form-data" in your form element!

  2. Hey Chris, Hey Guy,

    did you get this to run ? I am pretty confused, searched the almost whole net 'bout "couchdb upload attachment html form" and just found the way you described here. I posted in some forums and didn't get any response.

    This is what i did...

    ... and what i got from couchdb...

    Fehler 101 (net::ERR_CONNECTION_RESET):

    So i hope you can, and of course will, make it some clearer to me.

    Thanks in advance, Dennis

  3. @Dennis Been a while since I did this. Newer versions of CouchDB may do it completely differently. But I was able to get it working in a follow-up post the next day: