cURL PUT requests let you update resources on servers through the command line, but most tutorials stop at the basics.
This guide covers the tricks that matter: handling stubborn 411 errors and building smart retries for when APIs fight back.
You've probably used GET and POST requests a thousand times. But PUT requests? They're the unsung workhorses of RESTful APIs, ideal for updating entire resources idempotently. While everyone defaults to POST, PUT shines when you need consistency: upload the same file ten times and you'll get the same result, not ten copies.
Step 1: Start with the Fundamentals (But Skip the Fluff)
The basic PUT syntax looks deceptively simple:
curl -X PUT https://api.example.com/resource/123 -d "data"
But here's what most guides won't tell you: the -X PUT flag is often redundant. When you use certain options like -T for file uploads, curl automatically switches to PUT:
# These are equivalent for file uploads
curl -T myfile.json https://api.example.com/resource/123
curl -X PUT --data-binary @myfile.json https://api.example.com/resource/123
The real question is: which should you use? Go with -T for file uploads – it handles binary data better and doesn't load everything into memory first.
The JSON Trap
Sending JSON? Don't forget the Content-Type header, or you'll spend hours debugging why your perfectly valid JSON gets rejected:
curl -X PUT https://api.example.com/users/42 \
-H "Content-Type: application/json" \
-d '{"name":"John","age":30}'
Pro move: Use --data-raw instead of -d when your JSON contains special characters. The regular -d flag strips newlines and can mangle your data.
Step 2: Handle Authentication Like You Mean It
Bearer Tokens Without the Hassle
Most APIs use Bearer tokens these days. Here's the clean way to handle them:
# Store your token in an environment variable
export API_TOKEN="your_token_here"
# Use it in your requests
curl -X PUT https://api.example.com/resource \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status":"updated"}'
But what if your token expires mid-script? Here's a pattern that auto-refreshes:
#!/bin/bash
refresh_token() {
TOKEN=$(curl -s -X POST https://api.example.com/auth/refresh \
-d "refresh_token=$REFRESH_TOKEN" | jq -r '.access_token')
export API_TOKEN=$TOKEN
}
make_put_request() {
response=$(curl -w "\n%{http_code}" -X PUT https://api.example.com/resource \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d "$1")
http_code=$(echo "$response" | tail -n1)
if [ "$http_code" = "401" ]; then
refresh_token
make_put_request "$1" # Retry with new token
else
echo "$response" | head -n-1
fi
}
Multiple Auth Headers? No Problem
Some APIs require both basic auth for the server and bearer tokens for the application:
curl -X PUT https://api.example.com/resource \
-u "username:password" \
-H "X-API-Token: Bearer $TOKEN" \
-d @data.json
The trick? Use a custom header like X-API-Token when you need both. Most servers will accept this without complaint.
Step 3: Conquer the Dreaded 411 Error
Getting "411 Length Required" errors? You're not alone. This happens when servers demand a Content-Length header but curl doesn't provide one. Three fixes that actually work:
Fix 1: Add Empty Data
# Instead of this (which might fail)
curl -X PUT https://api.example.com/resource
# Do this
curl -X PUT https://api.example.com/resource -d ''
Fix 2: Calculate Content-Length Manually
data='{"key":"value"}'
length=${#data}
curl -X PUT https://api.example.com/resource \
-H "Content-Length: $length" \
-d "$data"
Fix 3: Use Chunked Transfer (When the Server Supports It)
curl -X PUT https://api.example.com/resource \
-H "Transfer-Encoding: chunked" \
-T large_file.bin
Step 4: Upload Large Files Without Drama
Uploading a 4GB file? Standard approaches will eat your RAM for breakfast. Here's how to do it right:
Stream Large Files
# DON'T do this (loads entire file into memory)
curl -X PUT https://api.example.com/upload --data-binary @huge_file.iso
# DO this instead (streams the file)
curl -T huge_file.iso https://api.example.com/upload
Split and Conquer
For truly massive files, split them first:
# Split into 100MB chunks
split -b 100M large_file.bin chunk_
# Upload each chunk with position headers
offset=0
for chunk in chunk_*; do
size=$(stat -f%z "$chunk" 2>/dev/null || stat -c%s "$chunk")
end=$((offset + size - 1))
curl -X PUT "https://api.example.com/upload?partNumber=$chunk" \
-H "Content-Range: bytes $offset-$end/*" \
-T "$chunk"
offset=$((end + 1))
done
Compress on the Fly
Why send 1GB when you can send 100MB?
# Compress and upload in one go
gzip -c large_file.json | curl -X PUT https://api.example.com/resource \
-H "Content-Encoding: gzip" \
-H "Content-Type: application/json" \
--data-binary @-
Step 5: Build Bulletproof Retry Logic
APIs fail. Networks hiccup. Here's how to handle it gracefully:
Smart Exponential Backoff
curl --retry 5 \
--retry-delay 1 \
--retry-max-time 60 \
--retry-all-errors \
-X PUT https://flaky-api.example.com/resource \
-d @data.json
But curl's built-in retry is limited. For production-grade retries with rate limit handling:
#!/bin/bash
put_with_retry() {
local url=$1
local data=$2
local max_retries=5
local retry_count=0
local delay=1
while [ $retry_count -lt $max_retries ]; do
response=$(curl -s -w "\n%{http_code}" -X PUT "$url" \
-H "Content-Type: application/json" \
-d "$data")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n-1)
case $http_code in
200|201|204)
echo "$body"
return 0
;;
429)
# Check for Retry-After header
retry_after=$(curl -sI -X PUT "$url" | grep -i "retry-after:" | cut -d' ' -f2 | tr -d '\r')
if [ -n "$retry_after" ]; then
delay=$retry_after
fi
echo "Rate limited. Waiting $delay seconds..." >&2
;;
5*)
echo "Server error. Retrying in $delay seconds..." >&2
;;
*)
echo "Request failed with code $http_code" >&2
echo "$body" >&2
return 1
;;
esac
sleep $delay
delay=$((delay * 2)) # Exponential backoff
retry_count=$((retry_count + 1))
done
echo "Max retries exceeded" >&2
return 1
}
The PUT vs PATCH Dilemma
Here's when to actually use PUT vs PATCH:
Use PUT when:
- Replacing entire resources
- You need idempotency guarantees
- Working with simple objects
- The API doesn't support PATCH
Use PATCH when:
- Updating specific fields
- Dealing with large objects where sending everything is wasteful
- You need to increment counters or apply relative changes
Real-world example – updating user profile:
# PUT - Send everything (even unchanged fields)
curl -X PUT https://api.example.com/users/123 \
-H "Content-Type: application/json" \
-d '{"name":"John","email":"john@example.com","age":30,"city":"NYC"}'
# PATCH - Send only what changed
curl -X PATCH https://api.example.com/users/123 \
-H "Content-Type: application/json-patch+json" \
-d '[{"op":"replace","path":"/age","value":31}]'
Bonus Tricks That Save Hours
Debug Without the Noise
# See exactly what curl sends/receives
curl -X PUT https://api.example.com/resource \
--trace-ascii - \
-d "test" 2>&1 | grep -E "^(>|<)"
Handle Redirects Properly
# Follow redirects but maintain PUT method
curl -L -X PUT https://api.example.com/resource \
-d @data.json \
--post301 --post302 --post303
Use Proxies for Debugging
# Route through local proxy to inspect traffic
curl -X PUT https://api.example.com/resource \
-x localhost:8080 \
-k # Ignore SSL errors from proxy
-d @data.json
Test Idempotency
# Run the same PUT request 3 times and compare responses
for i in {1..3}; do
echo "Request $i:"
curl -X PUT https://api.example.com/resource/999 \
-H "Content-Type: application/json" \
-d '{"status":"active"}' \
-s | md5sum
done
# If all checksums match, your PUT is properly idempotent
Common Pitfalls and Their Fixes
Empty Response with Large Files? Your server might be timing out. Add --expect100-timeout 0 to disable the Expect header:
curl -X PUT https://api.example.com/upload \
--expect100-timeout 0 \
-T large_file.bin
Getting 415 Unsupported Media Type? You forgot the Content-Type or sent the wrong one:
# Check what the server accepts
curl -X OPTIONS https://api.example.com/resource -i
# Then match it exactly
curl -X PUT https://api.example.com/resource \
-H "Content-Type: application/vnd.api+json" \
-d @data.json
PUT Creating Instead of Updating? Some APIs use PUT for both. Check the response code:
- 200/204 = Updated existing resource
- 201 = Created new resource
Next Steps
Now that you've mastered PUT requests, consider these advanced patterns:
- Implement conditional updates with ETags to prevent overwriting concurrent changes
- Build a wrapper script that handles authentication, retries, and logging automatically
- Explore HTTP/2 multiplexing for parallel PUT requests using
--http2
The difference between junior and senior developers isn't knowing PUT exists – it's knowing when -T beats --data-binary, why chunked encoding matters, and how to handle 429s without breaking a sweat.