Uploading file
You have built a simple HTTP server that can accept a file upload from the user and store the file in the server. Now, let’s build a simple client that can upload a file to the server.
The complete client uses only the standard library:
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
func main() {
if len(os.Args) != 2 {
log.Fatalf("usage: %s /path/to/file", os.Args[0])
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
if err := upload(ctx, http.DefaultClient, os.Args[1]); err != nil {
log.Fatal(err)
}
log.Print("upload completed")
}
func upload(ctx context.Context, client *http.Client, path string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open upload file: %w", err)
}
defer f.Close()
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
"http://localhost:8080/api/v1/binary",
f,
)
if err != nil {
return fmt.Errorf("create upload request: %w", err)
}
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("send upload request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
message, readErr := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
if readErr != nil {
return fmt.Errorf("upload failed with %s; read response: %w", resp.Status, readErr)
}
return fmt.Errorf("upload failed with %s: %s", resp.Status, strings.TrimSpace(string(message)))
}
if _, err := io.Copy(io.Discard, resp.Body); err != nil {
return fmt.Errorf("read upload response: %w", err)
}
return nil
}
Run the client with the file path as its only argument:
go run . /path/to/testfile
os.File implements io.Reader, so it can be streamed directly as the request body. The request context covers the entire request lifetime and cancels this introductory upload if it has not completed within two minutes. A real client should choose that deadline from its expected file sizes and network conditions.
The generic raw POST uses Content-Type: application/octet-stream. We will introduce the tus-specific application/offset+octet-stream media type later, when the client sends upload bytes in a tus PATCH request or uses the optional Creation With Upload POST extension. It should not be used for this non-tus endpoint.
http.Client.Do returns a response for HTTP error status codes, so a nil Go error does not mean the upload succeeded. The client closes every response body, rejects any status outside the 2xx range, and includes a bounded portion of an error response in the returned error. Reading a successful response to the end also allows the HTTP transport to reuse its connection.
When you run the client, you should see upload completed. You can also check the server logs to confirm that the entire file was stored. If the server returns an error such as 413 Request Entity Too Large, the client exits with that status and message instead of reporting success.
io.Reader and io.Writer interfaces are very powerful and can be used to build a lot of functionalities. In the next lesson, we will learn how to use these interfaces to build a more advanced file upload system. But for now, you have learned how to utilize these interfaces to upload a file to the server from a client.