Mert Özen Her Satırda Daha İleri
Geliştirici Araçları API Araçları — #02

curl ile API Test Etmek: Method, Header, --json, Authentication, Upload, Hata Kodları ve Script'te Kullanmak

Mert Özen 14 Kas 2026 14 dk 10 görüntülenme
curl ile API Test Etmek: Method, Header, --json, Authentication, Upload, Hata Kodları ve Script'te Kullanmak

curl'ü script'te güvenle kullanmanın kuralı, 401 ve 500'de de 0 dönen çıkış koduna güvenmemek: --fail-with-body hem 22 ile çıkar hem ProblemDetails'i basar. Bookstore.Api'ye karşı -v ve --trace-ascii, -d ile --json farkı (415), token'ı --variable ve -H @dosya ile history'den uzak tutmak, -F ile kapak yükleme ve antiforgery hatası, --retry sınırları, bir smoke script'i, Windows'ta curl.exe tırnak sorunu, Invoke-RestMethod ve xh gerçek çıktılarla.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; komutlar curl 8.x, jq 1.7, PowerShell 7.7 ve xh 0.26 ile çalıştırılmıştır. Windows PowerShell 5.1 ve Chrome DevTools'a özgü davranışlar resmi dokümanlardan aktarılmıştır.

Önce Tahmin Et

Bookstore.Api çalışıyor ve aşağıdaki üç komutu sırayla çalıştırıyorsun:

curl -X POST http://localhost:5080/api/auth/login -d '{"email":"ada@example.com","password":"Bookstore-Demo-1"}'
curl -s --fail http://localhost:5080/api/orders/1; echo "exit=$?"
curl -s http://localhost:5080/api/orders/1; echo "exit=$?"

Birinci komut token döndürür mü? İkinci ve üçüncü komutun ekrana ne bastığını ve çıkış kodunu tahmin et. Bir CI script'i bu komutlardan hangisine güvenirse hata kaçırır? Cevaplar ilgili bölümlerde, gerçek çıktılarla.

İsteği ve Cevabı Görmek: -i, -v ve --trace-ascii

curl varsayılan olarak yalnız response body'sini basar. API ile ilk konuşmada ihtiyacın olan şey status code ve header'lar; -i bunları body'nin önüne ekler (-s progress çubuğunu kapatır):

$ curl -si http://localhost:5080/api/books/9780261102217
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 14 Nov 2026 09:00:47 GMT
Server: Kestrel
Transfer-Encoding: chunked

{"isbn":"9780261102217","title":"The Hobbit","author":"J.R.R. Tolkien","category":"Fantasy","price":329.90,"publishedYear":1937,"description":"Bilbo Baggins leaves his comfortable home for an adventure with thirteen dwarves.","available":25}

Gönderdiğin isteği de görmek istediğinde -v kullanırsın. > ile başlayan satırlar curl'ün gönderdiği, < ile başlayanlar sunucunun döndürdüğü header'lar, * ile başlayanlar curl'ün kendi notlarıdır. Hepsi stderr'e yazılır, body stdout'ta kalır:

$ curl -sv http://localhost:5080/api/auth/login --json '{"email":"ada@example.com","password":"Bookstore-Demo-1"}' -o /dev/null
* Host localhost:5080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:5080...
* Connected to localhost (::1) port 5080
> POST /api/auth/login HTTP/1.1
> Host: localhost:5080
> User-Agent: curl/8.7.1
> Content-Type: application/json
> Accept: application/json
> Content-Length: 57
>
} [57 bytes data]
* upload completely sent off: 57 bytes
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sat, 14 Nov 2026 09:05:38 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
<
{ [402 bytes data]
* Connection #0 to host localhost left intact

-v body'yi göstermez. Gönderilen byte'ları da görmek gerekiyorsa --trace-ascii dosya her şeyi bir dosyaya yazar. Dosya yükleme bölümünde bunun neden işe yaradığını göreceğiz. HTTP mesajının satır satır formatı Networking ve HTTP serisinin "HTTP/1.1'i Kablodan Okumak" yazısında anlatılıyor.

Body Göndermek: -d, --json ve --data-urlencode

Tahmin sorusunun birinci komutu token döndürmez:

$ curl -si -X POST http://localhost:5080/api/auth/login -d '{"email":"ada@example.com","password":"Bookstore-Demo-1"}'
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/problem+json
Date: Sat, 14 Nov 2026 09:05:38 GMT
Server: Kestrel
Transfer-Encoding: chunked

{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.16","title":"Unsupported Media Type","status":415,"traceId":"00-22ee6168ff7f497b53bef9019819f590-74aabb0aeb047615-00"}

-d body'yi gönderir ama Content-Type'ı application/x-www-form-urlencoded yapar; minimal API JSON beklediği için 415 döner. Eski çözüm -H "Content-Type: application/json" eklemekti. curl 7.82.0'dan beri --json var: body'yi olduğu gibi gönderir, Content-Type: application/json ve Accept: application/json header'larını ekler ve method'u POST yapar. Yukarıdaki -v çıktısında bu iki header görünüyor. -X POST da gereksizleşiyor; PUT gerekiyorsa -X PUT eklemek yeterli. --json @order.json body'yi dosyadan, --json @- stdin'den okur. Bir uyarı: curl, verdiğin metnin geçerli JSON olup olmadığına bakmaz.

Query string'de boşluk ya da Türkçe karakter olan bir değer gönderirken elle encode etmek yerine -G ile --data-urlencode kullan. -G, -d ve --data-urlencode ile verilen parçaları body yerine URL'ye ekler:

$ curl -s -G http://localhost:5080/api/books --data-urlencode "search=war and peace" -d pageSize=2 -o /dev/null -w '%{url_effective}\n'
http://localhost:5080/api/books?search=war+and+peace&pageSize=2

Authentication: Bearer Token'ı Komut Satırında Bırakmamak

Bookstore.Api Bearer token bekliyor. En bilinen yazım -H "Authorization: Bearer $TOKEN"; curl'ün kendi seçeneği de var: --oauth2-bearer "$TOKEN". İkisi aynı header'ı üretir. Sorun şu: token komutun argümanı olunca shell history'ye düşer ve curl çalıştığı sürece ps çıktısında görünür. Değişken kullanmak history'yi kurtarır ama process listesini kurtarmaz. İki temiz yol var.

Birincisi header'ı dosyadan okumak. -H @dosya her satırı bir header olarak ekler:

$ umask 077
$ printf 'Authorization: Bearer %s\n' "$BOOKSTORE_TOKEN" > token.header
$ curl -s -H @token.header http://localhost:5080/api/auth/me
{"email":"ada@example.com","name":"Ada Lovelace","role":"Customer"}

İkincisi curl 8.3.0 ile gelen variable'lar. --variable %BOOKSTORE_TOKEN environment variable'ı curl'ün içine alır, --expand-header da header'ın içindeki {{BOOKSTORE_TOKEN}}'ı yerine koyar. Token ne komut satırında ne de bir dosyada görünür:

$ curl -s --variable %BOOKSTORE_TOKEN --expand-header "Authorization: Bearer {{BOOKSTORE_TOKEN}}" http://localhost:5080/api/auth/me
{"email":"ada@example.com","name":"Ada Lovelace","role":"Customer"}

-u user:password Basic authentication içindir. Bookstore.Api Basic kabul etmiyor, ama ne gönderildiğine bakmak öğretici:

$ curl -sv -u ada@example.com:Bookstore-Demo-1 http://localhost:5080/api/auth/me -o /dev/null 2>&1 | grep -E '^> Authorization|^< HTTP|^< WWW'
> Authorization: Basic YWRhQGV4YW1wbGUuY29tOkJvb2tzdG9yZS1EZW1vLTE=
< HTTP/1.1 401 Unauthorized
< WWW-Authenticate: Bearer
$ echo 'YWRhQGV4YW1wbGUuY29tOkJvb2tzdG9yZS1EZW1vLTE=' | base64 -d
ada@example.com:Bookstore-Demo-1

Basic, şifreyi yalnız base64 ile kodlar; HTTPS olmadan açık metinle aynıdır. Sunucunun WWW-Authenticate: Bearer cevabı da hangi şemayı beklediğini söylüyor.

Cookie ile oturum tutan uygulamalar (Identity'nin cookie login'i, server-side MVC (Model-View-Controller; sunucuda HTML üreten uygulama yapısı) uygulamaları) için -c jar.txt gelen cookie'leri dosyaya yazar, -b jar.txt sonraki istekte geri gönderir. Bookstore.Api cookie kullanmadığı için burada çıktı göstermiyorum; mantık token dosyasıyla aynı: jar dosyası da bir secret'tır, repo'ya girmez.

Dosya Yüklemek: -F ve multipart/form-data

Bu yazı için Bookstore.Api'ye kitap kapağı yükleme endpoint'i ekledim: PUT /api/books/{isbn}/cover, yalnız StoreStaff rolüne açık, JPEG (yaygın fotoğraf formatı) ya da PNG (kayıpsız görsel formatı) kabul ediyor ve 2 MB ile sınırlı. Content-Type'a güvenmeyip dosyanın ilk byte'larına da bakıyor:

public static RouteGroupBuilder MapBookCoverEndpoints(this RouteGroupBuilder catalog)
{
    // Bearer token var, cookie yok: başka bir sitedeki form token'ı gönderemez, antiforgery gerekmiyor.
    catalog.MapPut("/books/{isbn}/cover", UploadCover)
        .RequireAuthorization(policy => policy.RequireRole("StoreStaff"))
        .DisableAntiforgery();
    catalog.MapGet("/books/{isbn}/cover", GetCover);
    return catalog;
}

.DisableAntiforgery() satırı sonradan geldi. İlk denemede curl'den 500 döndü ve log şunu söyledi:

System.InvalidOperationException: Endpoint HTTP: PUT /api/books/{isbn}/cover => UploadCover contains anti-forgery metadata, but a middleware was not found that supports anti-forgery.

.NET 8'den beri IFormFile bağlayan minimal API endpoint'leri antiforgery doğrulaması ister. Cookie ile çalışan bir uygulamada bu doğru bir varsayılan; token'ı header'da taşıyan bir API'de gereksiz. Dosya yüklemenin limitleri ve streaming tarafı Web API serisinin "ASP.NET Core Web API'de Dosya Yükleme ve İndirme: IFormFile, Limitler ve Streaming" yazısında.

-F "alan=@dosya" multipart/form-data body'si kurar, method'u POST yapar; endpoint PUT olduğu için -X PUT ekliyoruz. Önce Ada (Customer), sonra Grace (StoreStaff) ile:

$ curl -si -X PUT $B/api/books/9780261102217/cover -H "Authorization: Bearer $ADA" -F "file=@the-hobbit.png" | head -1
HTTP/1.1 403 Forbidden
$ curl -s -X PUT $B/api/books/9780261102217/cover -H "Authorization: Bearer $GRACE" -F "file=@the-hobbit.png"
{"isbn":"9780261102217","fileName":"9780261102217.png","contentType":"image/png","size":838}
$ curl -s -o cover-back.png -w '%{http_code} %{content_type} %{size_download}\n' $B/api/books/9780261102217/cover
200 image/png 838

Şimdi içinde düz metin olan notes.png'yi yüklüyorum. curl, part'ın Content-Type'ını dosya uzantısından tahmin eder; --trace-ascii ne gittiğini gösteriyor:

$ curl -s --trace-ascii trace.txt -X PUT $B/api/books/9780261102217/cover -H "Authorization: Bearer $GRACE" -F "file=@notes.png"
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.16","title":"Unsupported Media Type","status":415,"detail":"Only JPEG and PNG covers are accepted.","traceId":"00-81ecab38ecee730fc716dd8a63658cb2-6fdc7cd59a1bce66-00"}
$ sed -n '/=> Send data/,/<= Recv header/p' trace.txt
=> Send data, 218 bytes (0xda)
0000: --------------------------HGOQzqvW2AXXBFGcxZE64i
0032: Content-Disposition: form-data; name="file"; filename="notes.png
0072: "
0075: Content-Type: image/png
008e:
0090: not really an image.
00a6: --------------------------HGOQzqvW2AXXBFGcxZE64i--

Part image/png olarak gitti; API'yi kurtaran, imza kontrolü. Yalnız file.ContentType'a bakan bir endpoint bu dosyayı kabul ederdi. Part'ın tipini kendin belirlemek istersen -F "file=@cover.jpg;type=image/jpeg" yazarsın.

Script'te curl: Çıkış Kodları, --fail-with-body ve --retry

Tahmin sorusunun ikinci ve üçüncü komutları:

$ curl -s http://localhost:5080/api/orders/1; echo " exit=$?"
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.2","title":"Unauthorized","status":401,"traceId":"00-313d5ccd5dc00c76ea989fa22e5ebac1-db9f584881551b79-00"} exit=0
$ curl -s --fail http://localhost:5080/api/orders/1; echo " exit=$?"
 exit=22
$ curl -sS --fail http://localhost:5080/api/orders/1; echo " exit=$?"
curl: (22) The requested URL returned error: 401
 exit=22
$ curl -s --fail-with-body http://localhost:5080/api/auth/login --json '{"email":"ada@example.com","password":"x"}'; echo " exit=$?"
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.2","title":"Unauthorized","status":401,"detail":"Invalid email or password.","traceId":"00-e903d1afc743adaad316c0df654539cf-2bb5012aff1c0e1e-00"} exit=22

Asıl tuzak burada: curl için 401 bir hata değil, başarıyla tamamlanmış bir HTTP konuşması; çıkış kodu 0. CI'da yalnız curl -s kullanan bir script, API'nin her isteğe 401 döndüğü gün yeşil kalır. --fail 400 ve üstü status'ta 22 ile çıkar ama body'yi yutar; -s ile birlikte hiçbir şey basmaz, -S eklenince en azından curl'ün hata satırı görünür. curl 7.76.0 ile gelen --fail-with-body ikisini birleştirir: 22 ile çıkar ve ProblemDetails body'sini de basar. Script'lerde varsayılanın bu olmalı.

Status code'u bir değişkene almak için body'yi dosyaya yönlendirip -w kullanırsın:

$ code=$(curl -s -o body.json -w '%{http_code}' http://localhost:5080/api/orders/1); echo "$code"
401

--retry N yalnız geçici hataları tekrar dener: timeout'lar ve 408, 429, 500, 502, 503, 504 gibi status'lar; Retry-After header'ını dikkate alır, beklemeyi her denemede ikiye katlar. Bağlantı reddi (API henüz açılmamış) varsayılan olarak geçici sayılmaz; onun için --retry-connrefused gerekir:

$ curl -sS --retry 3 http://localhost:5099/api/books; echo " exit=$?"
curl: (7) Failed to connect to localhost port 5099 after 0 ms: Couldn't connect to server
 exit=7
$ curl -sS --retry 3 --retry-connrefused --retry-delay 1 http://localhost:5099/api/books; echo " exit=$?"
curl: (7) Failed to connect to localhost port 5099 after 0 ms: Couldn't connect to server
curl: (7) Failed to connect to localhost port 5099 after 0 ms: Couldn't connect to server
curl: (7) Failed to connect to localhost port 5099 after 0 ms: Couldn't connect to server
curl: (7) Failed to connect to localhost port 5099 after 0 ms: Couldn't connect to server
 exit=7

--retry-all-errors diye bir seçenek de var; curl'ün kendi dokümanı onu kaba bir araç olarak tanımlıyor ve verinin iki kez gönderilebileceği konusunda uyarıyor. POST bir sipariş isteğini tekrar denemek iki sipariş demek olabilir. Kuralım şu: retry yalnız tekrarlanması güvenli isteklere. --max-time her denemenin üst sınırını koyar, aşılırsa çıkış kodu 28 olur. Bir isteğin hangi fazda yavaşladığını -w'nin zaman değişkenleriyle ölçmek Networking ve HTTP serisinin "Yavaş İstek Nereden Geliyor" yazısının konusu.

Bunları birleştirince ortak senaryoyu koşan, ilk hatada anlamlı bir mesajla duran bir smoke script'i çıkıyor. Repo'da scripts/smoke.sh:

#!/usr/bin/env bash
# Bookstore smoke testi: login -> kitaplar -> sepet -> sipariş -> siparişi oku.
# Kullanım: BOOKSTORE_PASSWORD=... ./scripts/smoke.sh   (BOOKSTORE_URL ve BOOKSTORE_EMAIL isteğe bağlı)
# curl 8.3+ (--variable) ve jq gerekir.
set -euo pipefail

base="${BOOKSTORE_URL:-http://localhost:5080}"
email="${BOOKSTORE_EMAIL:-ada@example.com}"
password="${BOOKSTORE_PASSWORD:?Set BOOKSTORE_PASSWORD}"

common=(--silent --show-error --fail-with-body --max-time 10)
# yalnız tekrarlanması güvenli istekler için
retry=(--retry 3 --retry-connrefused --retry-delay 1)
auth=(--variable %BOOKSTORE_TOKEN --expand-header "Authorization: Bearer {{BOOKSTORE_TOKEN}}")

# call <adım> <curl argümanları...>: body'yi basar; HTTP >= 400'de hata body'sini yazıp çıkar
call() {
    local step=$1 body
    shift
    if ! body=$(curl "${common[@]}" "$@"); then
        echo "FAIL  $step: $body" >&2
        exit 1
    fi
    printf '%s' "$body"
}

login_body=$(jq -n --arg email "$email" --arg password "$password" '{email: $email, password: $password}')
BOOKSTORE_TOKEN=$(call login "${retry[@]}" "$base/api/auth/login" --json "$login_body" | jq -r .accessToken)
export BOOKSTORE_TOKEN
echo "ok    login as $email"

isbn=$(call books "${retry[@]}" "$base/api/books?page=1&search=Hobbit" | jq -r '.items[0].isbn')
echo "ok    book $isbn"

cart=$(call cart "${auth[@]}" -X POST "$base/api/carts" | jq -r .id)
total=$(call add-item "${auth[@]}" -X PUT "$base/api/carts/$cart/items/$isbn" --json '{"quantity":2}' | jq -r .total)
echo "ok    cart $cart total $total"

order_body=$(jq -n --arg cart "$cart" --arg email "$email" '{cartId: $cart, customerName: "Smoke Test", customerEmail: $email}')
location=$(call order "${auth[@]}" "$base/api/orders" --json "$order_body" -o /dev/null -w '%header{location}')
echo "ok    order $location"

status=$(call read-order "${auth[@]}" "${retry[@]}" "$location" | jq -r .status)
if [[ "$status" != "Placed" ]]; then
    echo "FAIL  read-order: expected Placed, got $status" >&2
    exit 1
fi
echo "ok    status $status"

Birkaç karar: JSON body'leri jq -n --arg ile kuruluyor; şifrede tırnak ya da ters bölü olsa bile JSON bozulmuyor. Sipariş adresi -w '%header{location}' ile doğrudan Location header'ından alınıyor (curl 7.84.0). POST istekleri retry almıyor. jq'nun kendisi Terminal ve Command Line serisinde ayrıntılı anlatılacak. Üç koşu:

$ BOOKSTORE_PASSWORD=Bookstore-Demo-1 ./scripts/smoke.sh; echo "exit=$?"
ok    login as ada@example.com
ok    book 9780261102217
ok    cart 01a1d94a-b8bd-7321-8836-8f8308bcfd7d total 659.80
ok    order http://localhost:5080/api/orders/1
ok    status Placed
exit=0
$ BOOKSTORE_PASSWORD=wrong ./scripts/smoke.sh; echo "exit=$?"
curl: (22) The requested URL returned error: 401
FAIL  login: {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.2","title":"Unauthorized","status":401,"detail":"Invalid email or password.","traceId":"00-e79496c710ff3a2555dce00168ddaaf1-8e93d51d077f3a3d-00"}
exit=1
$ ./scripts/smoke.sh; echo "exit=$?"
./scripts/smoke.sh: line 9: BOOKSTORE_PASSWORD: Set BOOKSTORE_PASSWORD
exit=1

Windows'ta curl.exe ve PowerShell

Windows 10 1803'ten beri C:\Windows\System32\curl.exe işletim sistemiyle geliyor ve Windows Update ile güncelleniyor; 2026'daki Windows 11 sürümlerinde 8.x serisi. Dikkat edilecek iki nokta var.

Birincisi isim. Windows PowerShell 5.1'de curl, Invoke-WebRequest'in alias'ıdır; curl -s ... yazınca curl değil cmdlet çalışır ve parametreleri tanımaz. PowerShell 7 bu alias'ı tanımlamıyor. Her iki sürümde de güvenli yazım curl.exe.

İkincisi tırnaklar. PowerShell 7.3 native programlara argüman geçirme şeklini değiştirdi ve bunu $PSNativeCommandArgumentPassing ile yönetilebilir yaptı. Windows'ta varsayılan Windows, diğer platformlarda Standard; curl.exe için ikisi aynı davranır ve JSON'daki çift tırnaklar korunur. Windows PowerShell 5.1'in davranışı ise eski (Legacy) davranış: çift tırnaklar yolda kaybolur. PowerShell 7.7'de iki modu aynı komutla denedim:

PS> curl -s "$base/api/auth/login" --json '{"email":"ada@example.com","password":"Bookstore-Demo-1"}'
{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5...
PS> $PSNativeCommandArgumentPassing = 'Legacy'
PS> curl -s "$base/api/auth/login" --json '{"email":"ada@example.com","password":"Bookstore-Demo-1"}'
{"type":"https://tools.ietf.org/html/rfc9110#section-15.6.1","title":"An error occurred while processing your request.","status":500,"traceId":"00-15fb2253e7da905120068ef7a83b6407-76ba2e638e1cf814-00"}

API log'u sebebi gösteriyor: gelen body {email:ada@example.com,...} olmuş, 'e' is an invalid start of a property name. 400 yerine 500 dönmesi de Development'a özgü: minimal API'ler Development'ta bozuk body için BadHttpRequestException fırlatır, exception handler bunu 500'e çevirir. 5.1'de çalışmak zorundaysan body'yi dosyaya yazıp --json "@login.json" ile vermek tırnak sorununu tamamen ortadan kaldırır.

PowerShell'de çoğu zaman curl yerine Invoke-RestMethod daha rahattır: JSON cevabı object'e çevirir, body'yi hashtable'dan kurarsın, tırnak sorunu kalmaz:

$base = 'http://localhost:5080'
$body = @{ email = 'ada@example.com'; password = $env:BOOKSTORE_PASSWORD } | ConvertTo-Json
$login = Invoke-RestMethod -Method Post -Uri "$base/api/auth/login" -ContentType 'application/json' -Body $body
$token = ConvertTo-SecureString $login.accessToken -AsPlainText -Force
(Invoke-RestMethod "$base/api/books?page=1&pageSize=3").items | Format-Table isbn, title, price
Invoke-RestMethod "$base/api/orders/1" -Authentication Bearer -Token $token -AllowUnencryptedAuthentication |
    Format-List id, status, total
isbn          title             price
----          -----             -----
9780451524935 1984            279.000
9780143035008 Anna Karenina   429.000
9780060850524 Brave New World 299.000

id     : 1
status : Placed
total  : 659.8

Son satırdaki -AllowUnencryptedAuthentication ilk denemede yoktu ve cmdlet isteği hiç göndermedi: "The cmdlet cannot protect plain text secrets sent over unencrypted connections" hatası verdi. -Authentication Bearer HTTP üzerinden token göndermeyi reddediyor; yerel geliştirmede bu parametreyle açıkça izin verirsin, HTTPS'te gerekmez. Hata durumunda Invoke-RestMethod exception fırlatır; script'te status code'u almak için -SkipHttpErrorCheck -StatusCodeVariable status kullanırsın:

PS> $problem = Invoke-RestMethod "$base/api/orders/1" -SkipHttpErrorCheck -StatusCodeVariable status
PS> "$status $($problem.title)"
401 Unauthorized

Copy as cURL ve Daha Okunur Alternatifler

Bir hatayı reproduce etmenin en kısa yolu tarayıcıdan başlıyor: Network panelinde isteğe sağ tıklayıp Copy as cURL (Browser DevTools serisinin "Network Paneli" yazısında). Windows'ta Chrome bu menüyü "cURL (cmd)" ve "cURL (bash)" diye ikiye ayırıyor; cmd sürümü özel karakterleri ^ ile kaçırır, bash sürümü tek tırnak kullanır. PowerShell'e yapıştıracaksan "Copy as PowerShell" daha az sorun çıkarır. Kopyalanan komut tarayıcının bütün header'larını ve cookie'lerini taşır; issue'ya eklemeden önce Authorization ve Cookie satırlarını sil.

Gündelik keşif için curl'ün syntax'ı uzun gelebilir. httpie ve onun Rust ile yazılmış, tek dosyalık uyumlu sürümü xh, field=value yazımından JSON body kurar ve cevabı renklendirip biçimlendirir:

$ xh --pretty=format -b :5080/api/books page==1 pageSize==2 search=="war and peace"
{
    "items": [
        {
            "isbn": "9781400079988",
            "title": "War and Peace",
            "author": "Leo Tolstoy",
            "category": "Classics",
            "price": 689.0,
            "publishedYear": 1869
        }
    ],
    "total": 1,
    "page": 1,
    "pageSize": 2
}
$ xh --offline --pretty=format POST :5080/api/carts/x/items/y quantity:=2
POST /api/carts/x/items/y HTTP/1.1
...
Content-Type: application/json
User-Agent: xh/0.26.3

{
    "quantity": 2
}

== query string, = string alan, := ham JSON değeri demek; --offline isteği göndermeden yazdırır. Bir ayrıntıya dikkat: API 689.00 döndürdü, xh'nin formatlayıcısı 689.0 gösterdi. Biçimlendiriciler sayıyı yeniden yazabilir; decimal hassasiyetini kontrol ediyorsan ham body'ye bak (--pretty=none). Bu araçlar keşifte rahat, ama her CI runner'ında ve her Windows makinesinde hazır bulunan curl kadar taşınabilir değil.

Üretimde Dikkat

Production'a karşı curl çalıştırmak gerekiyorsa (bir incident sırasında health check, bir webhook'u elle tetiklemek) komutu önce bir arkadaşına göster, -X DELETE ve PUT isteklerini iki kez oku. Token'ı -H @dosya ya da --variable ile ver; --trace-ascii ve -v çıktısı Authorization header'ını açıkça yazar, bu çıktıları bir ticket'a ya da sohbete yapıştırmadan önce temizle.

Smoke script'ini CI'da koşarken --max-time vermeyi unutma. Varsayılan olarak curl'ün toplam süre sınırı yok; yanıt vermeyen bir API, job'u runner'ın kendi timeout'una kadar bekletir. Retry'ları da sınırlı tut: üç deneme ve kısa bir gecikme, API ayağa kalkarken yeterli; daha fazlası gerçek bir hatayı dakikalarca geciktirir.

Bir de -k (--insecure): certificate doğrulamasını kapatır. Yerelde self-signed bir certificate ile denerken cazip gelir, ama script'e girdiği gün production'da da kapalı kalır. Yerel geliştirmede dotnet dev-certs https --trust ile güvenilen bir certificate kullan ya da --cacert ile o certificate'ı ver.

Sık Düşülen Tuzaklar

-d ile JSON gönderip 415 almak: --json kullan ya da Content-Type'ı açıkça ver.

curl -s'in çıkış koduna güvenmek: 401 ve 500 de 0 döner. Script'te --fail-with-body kullan.

Token'ı komut satırına yazmak: shell history'ye ve process listesine düşer. -H @dosya ya da --variable %ENV kullan. Yanlışlıkla yazdıysan bash'te history -d ile o satırı sil ve token'ı yenile.

POST isteğine --retry-all-errors eklemek: timeout sonrası tekrar, sunucunun aslında işlediği bir siparişi ikinci kez oluşturabilir. Idempotent olmayan isteklerde retry'ı kapalı tut ya da API tarafında idempotency key kullan (API Güvenliği serisinin API6 (hassas iş akışlarıyla ilgili 6. madde) yazısında anlatılıyor).

Windows PowerShell 5.1'de curl yazıp Invoke-WebRequest çalıştırmak ve tek tırnaklı JSON'un bozulması: curl.exe yaz, body'yi dosyadan ver ya da Invoke-RestMethod kullan.

Küçük Bir Deneme

★☆☆ curl -sv ile Grace olarak login ol, gelen token'ı BOOKSTORE_TOKEN'a koy ve --variable ile GET /api/auth/me çağır. Ardından aynı isteği token olmadan at ve WWW-Authenticate header'ını bul.

★★☆ smoke.sh'ye bir adım ekle: Grace olarak giriş yapıp kitabın kapağını -F ile yüklesin ve GET /api/books/{isbn}/cover'ın image/png döndürdüğünü -w '%{content_type}' ile kontrol etsin. Ada'nın token'ıyla aynı yüklemenin script'i 403 ile durdurduğunu gör.

★★★ Script'in PowerShell 7 karşılığını Invoke-RestMethod ile yaz: hata durumunda ProblemDetails'in detail alanını yazdırsın ve 1 ile çıksın. Aynı script'i $PSNativeCommandArgumentPassing = 'Legacy' iken curl.exe ile yazmayı dene ve hangi satırların bozulduğunu not et.

Bu yazıdaki akış çalışıyor ama bash'e ve jq'ya bağlı, IDE'de tek tıkla çalışmıyor. Bir sonraki yazıda aynı akışı repo'da duran bir .http dosyasına taşıyoruz: Visual Studio, Rider ve Visual Studio Code'da variable, environment ve request chaining, bir de JetBrains'in ijhttp CLI'ı ile gerçek bir koşu.