Skip to content

Instantly share code, notes, and snippets.

@mattetti
Last active February 18, 2026 02:21
Show Gist options
  • Select an option

  • Save mattetti/5914158 to your computer and use it in GitHub Desktop.

Select an option

Save mattetti/5914158 to your computer and use it in GitHub Desktop.
Example of doing a multipart upload in Go (golang)
package main
import (
"bytes"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
// Creates a new file upload http request with optional extra params
func newfileUploadRequest(uri string, params map[string]string, paramName, path string) (*http.Request, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile(paramName, filepath.Base(path))
if err != nil {
return nil, err
}
_, err = io.Copy(part, file)
for key, val := range params {
_ = writer.WriteField(key, val)
}
err = writer.Close()
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", uri, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
return req, err
}
func main() {
path, _ := os.Getwd()
path += "/test.pdf"
extraParams := map[string]string{
"title": "My Document",
"author": "Matt Aimonetti",
"description": "A document with all the Go programming language secrets",
}
request, err := newfileUploadRequest("https://google.com/upload", extraParams, "file", "/tmp/doc.pdf")
if err != nil {
log.Fatal(err)
}
client := &http.Client{}
resp, err := client.Do(request)
if err != nil {
log.Fatal(err)
} else {
body := &bytes.Buffer{}
_, err := body.ReadFrom(resp.Body)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
fmt.Println(resp.StatusCode)
fmt.Println(resp.Header)
fmt.Println(body)
}
}
@pforpramit
Copy link

@sebnyberg Thank you very much! :)

@nirajchandak
Copy link

If uploading specific format like gzip or server expect content other than octet-stream, better to use writer.CreatePart instead of formWriter.CreateFormFile

	part, err := writer.CreatePart(textproto.MIMEHeader{
		"Content-Type": []string{"application/x-gzip"},
		"Content-Disposition": []string{fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
			"file", fi.Name())},
	})

@isauran
Copy link

isauran commented Oct 12, 2025

@coolaj86
Copy link

coolaj86 commented Feb 18, 2026

One of the core problems with the original example is that it requires buffering all data into into memory, which doesn't work very well if you're dealing with files.

However, it's non-obvious how to use the multi-part form in a streaming fashion - in part because the design of it is to enclose the various parts with a header and footer, which doesn't lend itself well to a typical streaming interface.

Instead, you need to create a pipe connected to the form and then and then io.Copy each file into that pipe after setting the headers, and then close it all out at the end.

Key notes:

  • need to create a Pipe with which to construct the http request
  • create a form using the pipe
  • set boundary header
  • start a goroutine that begins feeding the pipe (i.e. with a file) before Do()ing the request
  • must call form.Close() to add the final boundary and complete the body
// Written in 2026 by AJ ONeal <aj@therootcompany.com> with assistance by Grok Ai.
// To the extent possible under law, the author(s) have dedicated all copyright
// and related and neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along with
// this software. If not, see <https://creativecommons.org/publicdomain/zero/1.0/>.

func (c *Client) UploadFile(folderID string, r io.ReadCloser, filename string) (*FileInfo, error) {
	if filename == "" {
		_ = r.Close()
		return nil, errors.New("filename is required")
	}
	defer func() { _ = r.Close() }()

	token, err := c.getToken()
	if err != nil {
		return nil, err
	}

	pr, pw := io.Pipe()
	form := multipart.NewWriter(pw)
	defer func() {
		// safe to do here because the goroutine must end before the request
		_ = pr.Close()
		_ = pw.Close()
		_ = form.Close()
	}()

	// fmt.Println("DEBUG create request", uploadURL)
	uploadURL := DefaultUploadURL
	req, err := http.NewRequest("POST", uploadURL, pr)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "multipart/form-data; boundary="+form.Boundary())

	// Since the file can only be copied once it's already being read,
	// we need this goroutine to allow us to submit the request.
	// This wouldn't be necessary if we were just uploading a file that
	// could be read from directly, or if we didn't have the final boundary
	// and could create use a concatenating reader
	go func() {
		// meta data
		part, _ := form.CreateFormField("attributes")
		attributes := map[string]any{
			"name":   filename,
			"parent": map[string]string{"id": folderID},
		}
		attrsJSON, _ := json.Marshal(attributes)
		if _, err = part.Write(attrsJSON); err != nil {
			pw.CloseWithError(err)
			return
		}
		// fmt.Println("DEBUG wrote attrs")

		// file data
		filePart, _ := form.CreateFormFile("file", filename)
		if _, err = io.Copy(filePart, r); err != nil {
			pw.CloseWithError(err)
			return
		}
		// fmt.Println("DEBUG wrote file")

		// form data
		if err := form.Close(); err != nil {
			pw.CloseWithError(err)
			return
		}
		// fmt.Println("DEBUG wrote form")
	}()

	// fmt.Println("DEBUG do request")
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("upload request failed: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	// fmt.Println("DEBUG got response")
	if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
		bodyBytes, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("upload failed (%s): %s", resp.Status, string(bodyBytes))
	}

	var uploadResp struct {
		Entries []FileInfo `json:"entries"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&uploadResp); err != nil {
		return nil, fmt.Errorf("decode upload response: %w", err)
	}
	if len(uploadResp.Entries) == 0 {
		return nil, errors.New("no file entry in upload response")
	}
	return &uploadResp.Entries[0], nil
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment