go / cloudflare
Cloudflare Images stores images and serves them through Cloudflare's CDN. I use it to store logos and avatars uploaded by users or fetched from third-party APIs.
The client wraps two endpoints: direct image uploads from the server and one-time upload URLs for client-side uploads.
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},
}
}
The retry and transient-code plumbing lives in a shared
backoff helper. Each call builds a request, hands
it to httputil.Do, and maps the response shape to a result.
// 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
}
UploadImage retries with backoff because image uploads are
idempotent and Cloudflare returns transient 502/503/504 and
rate-limit 429 responses under load. The retry loop turns most
of those into eventual successes. retryDelays = {1s, 2s, 0}
keeps total backoff short. Background workers use longer delays.
multipart.CreateFormFile hardcodes
Content-Type: application/octet-stream, so I build the part
header by hand to carry the detected MIME type.
Server-side upload
The worker downloads an image from a third-party URL, validates it, uploads it to Cloudflare, and rewrites the stored URL:
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
swap in fakes without touching the network.
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
Fetches to attacker-supplied URLs need a guard. Without one, an
image URL like http://localhost:5432/ or
http://169.254.169.254/latest/meta-data/ would trick the
server into reaching internal services or cloud metadata.
safeURL parses the URL, requires http/https, and rejects
loopback and localhost hosts before the HTTP client connects.
Any handler that fetches an attacker-supplied URL goes through
it.
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
}
Magic bytes content-type detection
Trust the bytes, not the headers:
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 ""
}
A Content-Type: image/png header proves nothing about the
payload. The first few bytes do.
Direct creator upload
For browser uploads, generate a one-time URL the client POSTs to directly. The server never sees the image 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 receives {upload_url, image_id, public_url}, POSTs
the file to upload_url, and uses public_url once the upload
completes.
Rate limits
Cloudflare's API allows 1200 requests per 5 minutes and blocks
for 5 minutes if exceeded. The worker's throttle sets the
delay before the next job based on the last job's status:
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
Stub the HTTP boundary with httptest.NewServer. The retry loop
is easy to script with an atomic counter that fails the first
attempts and then succeeds:
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)
}
For the upload job, the Uploader and HTTPDoer interfaces take
fakes, so the whole pipeline runs without a network or a real
Cloudflare account.