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)
}