go / cloudflare

Cloudflare Images stores and serves images. I use it for logos and avatars that users upload or that my code fetches from a third-party API.

The client wraps two endpoints: server-side uploads and one-time upload URLs for the browser.

Client

cloudflare.Client:

const defaultAPIBase = "https://api.cloudflare.com/client/v4"

var retryDelays = []time.Duration{time.Second, 2 * time.Second, 0}
var transientCodes = httputil.WithTransientCodes(429)

type Client struct {
	accountID   string
	apiToken    string
	apiBase     string
	accountHash string
	http        *http.Client
}

func NewClient(accountID, apiToken, accountHash string) *Client {
	return &Client{
		accountID:   accountID,
		apiToken:    apiToken,
		apiBase:     defaultAPIBase,
		accountHash: accountHash,
		http:        &http.Client{Timeout: 20 * time.Second},
	}
}

Each call builds a request, hands it to the backoff helper httputil.Do, and maps the response:

// https://developers.cloudflare.com/api/operations/cloudflare-images-upload-an-image-via-url
func (c *Client) UploadImage(ctx context.Context, body []byte, contentType, filename string) (UploadResult, error) {
	url := fmt.Sprintf("%s/accounts/%s/images/v1", c.apiBase, c.accountID)
	res, err := httputil.Do(ctx, func() (*http.Request, error) {
		form := &bytes.Buffer{}
		writer := multipart.NewWriter(form)
		// Set the detected MIME type on the file part. CreateFormFile
		// would default to application/octet-stream and lose the
		// signal for providers that infer from the part headers.
		header := make(textproto.MIMEHeader)
		header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, escapeQuotes(filename)))
		header.Set("Content-Type", contentType)
		part, err := writer.CreatePart(header)
		if err != nil {
			return nil, err
		}
		if _, err := part.Write(body); err != nil {
			return nil, err
		}
		if err := writer.Close(); err != nil {
			return nil, err
		}
		req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, form)
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Bearer "+c.apiToken)
		req.Header.Set("Content-Type", writer.FormDataContentType())
		return req, nil
	}, httputil.Config{
		Client:         c.http,
		RetryDelays:    retryDelays,
		TransientCodes: transientCodes,
	})
	if err != nil {
		return UploadResult{}, err
	}
	if res.StatusCode/100 != 2 {
		return UploadResult{}, fmt.Errorf("HTTP %d", res.StatusCode)
	}

	var parsed struct {
		Success bool `json:"success"`
		Result  struct {
			ID       string `json:"id"`
			Filename string `json:"filename"`
		} `json:"result"`
	}
	if err := json.Unmarshal(res.Body, &parsed); err != nil {
		return UploadResult{}, fmt.Errorf("JSON parse error")
	}
	if !parsed.Success {
		return UploadResult{}, fmt.Errorf("API returned success=false")
	}
	return UploadResult{ImageID: parsed.Result.ID, Filename: parsed.Result.Filename}, nil
}

An image upload is idempotent, and Cloudflare returns 502/503/504 and 429 under load, so UploadImage retries. retryDelays = {1s, 2s, 0} keeps the total short.

Server-side upload

The worker fetches an image from a third-party URL, validates it, and uploads it to Cloudflare:

func UploadImage(
	ctx context.Context,
	db *pgdb.DB,
	uploader Uploader,
	fetcher HTTPDoer,
	cfg ImageConfig,
	args UploadImageArgs,
) (string, error) {
	src := strings.TrimSpace(args.ImageURL)
	if !safeURL(src) {
		return "err: unsafe URL", nil
	}

	body, status, err := fetchImage(ctx, fetcher, src)
	if err != nil {
		return status, nil
	}

	contentType := detectContentType(body)
	if contentType == "" {
		return "err: unsupported file type", nil
	}
	if len(body) > maxBytes {
		return "err: image too large", nil
	}

	digest := sha256.Sum256(body)
	filename := fmt.Sprintf("%s-%d-%s.%s", args.Table, args.ID,
		hex.EncodeToString(digest[:])[:16], extensionForContentType(contentType))

	result, err := uploader.UploadImage(ctx, body, contentType, filename)
	if err != nil {
		return "err: Cloudflare upload failed " + err.Error(), nil
	}
	return "ok", nil
}

Uploader and HTTPDoer are one-method interfaces, so tests inject fakes:

type Uploader interface {
	UploadImage(ctx context.Context, body []byte, contentType, filename string) (UploadResult, error)
}

type HTTPDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

SSRF guard

Without a guard, an image URL like http://localhost:5432/ reaches an internal service. safeURL requires http or https and rejects loopback hosts:

func safeURL(raw string) bool {
	parsed, err := url.Parse(raw)
	if err != nil {
		return false
	}
	if parsed.Scheme != "http" && parsed.Scheme != "https" {
		return false
	}
	host := strings.ToLower(parsed.Hostname())
	if host == "" || host == "localhost" || host == "::1" {
		return false
	}
	if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() {
		return false
	}
	return true
}

Content-type detection

The worker ignores the Content-Type header and reads the first bytes:

func detectContentType(body []byte) string {
	if len(body) >= 3 && body[0] == 0xFF && body[1] == 0xD8 && body[2] == 0xFF {
		return "image/jpeg"
	}
	if len(body) >= 4 && bytes.Equal(body[:4], []byte{0x89, 0x50, 0x4E, 0x47}) {
		return "image/png"
	}
	if len(body) >= 3 && bytes.Equal(body[:3], []byte{0x47, 0x49, 0x46}) {
		return "image/gif"
	}
	if len(body) >= 12 && bytes.Equal(body[:4], []byte{0x52, 0x49, 0x46, 0x46}) &&
		bytes.Equal(body[8:12], []byte{0x57, 0x45, 0x42, 0x50}) {
		return "image/webp"
	}
	if bytes.HasPrefix(body, []byte("<?xml")) || bytes.HasPrefix(body, []byte("<svg")) {
		return "image/svg+xml"
	}
	return ""
}

Direct creator upload

A one-time URL lets the browser POST the file directly, so the server never sees the bytes:

func (c *Client) GetDirectUploadURL(ctx context.Context) (DirectUploadResult, error) {
	url := fmt.Sprintf("%s/accounts/%s/images/v2/direct_upload", c.apiBase, c.accountID)
	res, err := httputil.Do(ctx, func() (*http.Request, error) {
		req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Bearer "+c.apiToken)
		return req, nil
	}, httputil.Config{
		Client:         c.http,
		RetryDelays:    retryDelays,
		TransientCodes: transientCodes,
	})
	if err != nil {
		return DirectUploadResult{}, err
	}
	if res.StatusCode/100 != 2 {
		return DirectUploadResult{}, fmt.Errorf("HTTP %d", res.StatusCode)
	}

	var parsed struct {
		Success bool `json:"success"`
		Result  struct {
			UploadURL string `json:"uploadURL"`
			ID        string `json:"id"`
		} `json:"result"`
	}
	if err := json.Unmarshal(res.Body, &parsed); err != nil {
		return DirectUploadResult{}, fmt.Errorf("JSON parse error")
	}
	if !parsed.Success {
		return DirectUploadResult{}, fmt.Errorf("API returned success=false")
	}
	return DirectUploadResult{
		UploadURL: parsed.Result.UploadURL,
		ImageID:   parsed.Result.ID,
		PublicURL: fmt.Sprintf("https://%s/cdn-cgi/imagedelivery/%s/%s/public", imageDomain, c.accountHash, parsed.Result.ID),
	}, nil
}

The browser POSTs the file to upload_url and then uses public_url.

Rate limits

Cloudflare allows 1200 requests per 5 minutes and blocks for 5 minutes after a client goes over. The worker's throttle reads the last job's status to set the next delay:

const (
	maxJobsPerSecond = 4
	rateLimitBackoff = 5 * time.Minute
)

func throttle(status string, _ error, elapsed time.Duration) time.Duration {
	if strings.Contains(status, "429 Too Many Requests") {
		if elapsed >= rateLimitBackoff {
			return 0
		}
		return rateLimitBackoff - elapsed // pause to clear the block
	}
	minJobTime := time.Second / maxJobsPerSecond
	if elapsed >= minJobTime {
		return 0
	}
	return minJobTime - elapsed
}

Tests

Tests point the client at an httptest.NewServer. A counter fails the first two attempts:

func TestClientUploadImageRetries429(t *testing.T) {
	var calls atomic.Int32
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		if calls.Add(1) <= 2 {
			w.WriteHeader(429)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]any{
			"success": true,
			"result":  map[string]string{"id": "img-1", "filename": "x.png"},
		})
	}))
	defer srv.Close()

	c := NewClient("acct", "tok", "hash")
	c.apiBase = srv.URL
	c.http = srv.Client()

	_, err := c.UploadImage(context.Background(), []byte("png-bytes"), "image/png", "logo.png")
	tu.OK(err == nil)
	tu.OK(calls.Load() == 3)
}

← All articles