Emacs Lisp: Making a multi-part form PUT or POST using url-retrieve-synchronously and mm-url-encode-multipart-form-data

| emacs

I spent some time figuring out how to submit a multipart/form-data form with url-retrieve-synchronously with Emacs Lisp. It was surprisingly hard to find an example of working with multi-part forms. I had totally forgotten that I had figured something out last year: Using Emacs Lisp to export TXT/EPUB/PDF from Org Mode to the Supernote via Browse and Access. Well, I still had to spend some extra time dealing with the quirks of the PeerTube REST API. For toobnix.org, having = in the boundary didn't seem to work. Also, since I had newlines (\n) in my data, I needed to replace all of them with \r\n, which I could do with encode-coding-string and the utf-8-dos coding system. So here's an example I can use for the future:

(let* ((boundary (format "%s%d" (make-string 20 ?-) (time-to-seconds)))
       (url-request-method "PUT")       ; or POST
       (url-request-extra-headers
        (append
         (list
          (cons "Content-Type"
                (concat "multipart/form-data; boundary=" boundary))
          ;; put any authentication things you need here too, like
          ;; (cons "Authorization" "Bearer ...")
          )
         url-request-extra-headers
         nil))
       (url-request-data
        (mm-url-encode-multipart-form-data
         `(("field1" .
            ,(encode-coding-string "Whatever\nyour value is" 'utf-8-dos)))
         boundary))
       (url "http://127.0.0.1"))        ; or whatever the URL is
    (with-current-buffer (url-retrieve-synchronously url)
      (prog1 (buffer-string)
        (kill-buffer (current-buffer)))))

I've also added it to my local elisp-demos notes file (see the elisp-demos-user-files variable) so that helpful can display it when I use C-h f to describe mm-url-encode-multipart-form-data.

Here I'm using it to update the video description in emacsconf-toobnix.el:

emacsconf-toobnix-update-video-description: Update the description for TALK.
(defun emacsconf-toobnix-update-video-description (talk &optional type)
  "Update the description for TALK.
TYPE is 'talk or 'answers."
  (interactive
   (let ((talk (emacsconf-complete-talk-info)))
     (list
      talk
      (if (plist-get talk :qa-toobnix-url)
          (intern (completing-read "Type: " '("talk" "answers")))
        'talk))))
  (setq type (or type 'talk))
  (let* ((properties
          (pcase type
            ('answers (emacsconf-publish-answers-video-properties talk 'toobnix))
            (_ (emacsconf-publish-talk-video-properties talk 'toobnix))))
         (id
          (emacsconf-toobnix-id-from-url
           (plist-get talk (pcase type
                             ('answers :qa-toobnix-url)
                             (_ :toobnix-url)))))
         (boundary (format "%s%d" (make-string 20 ?-)
                           (time-to-seconds)))
         (url-request-method "PUT")
         (url-request-extra-headers
          (cons (cons "Content-Type"
                      (concat "multipart/form-data; boundary=" boundary))
                (emacsconf-toobnix-api-header)))
         (url-request-data
          (mm-url-encode-multipart-form-data
                     `(("description" .
                        ,(encode-coding-string
                          (plist-get properties :description)
                          'utf-8-dos)))
                     boundary))
         (url (concat "https://toobnix.org/api/v1/videos/" id)))
    (with-current-buffer (url-retrieve-synchronously url)
      (prog1 (buffer-string)
        (kill-buffer (current-buffer))))))

View org source for this post