טיפול בשגיאות וניסיונות חוזרים
כל שגיאה ב-v2 מגיעה במעטפת יציבה אחת עם code קריא-מכונה, כך שהאינטגרציה שלכם יכולה להסתעף לפי קודים במקום לפרסר טקסט חופשי. עמוד זה הוא ספר ההפעלה התפעולי: מה כל קוד אומר, מה לעשות איתו, ואיך לנסות שוב יצירת משלוח בבטחה בלי חיוב כפול. מפרט המעטפת המלא נמצא במדריך השגיאות.
שלב 1 — פענוח המעטפת
תגובות מוצלחות עוטפות את ה-payload שלהן ב-{ "data": ... }. כל שגיאה (בכל endpoint תחת api/v2/*) משתמשת ב:
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid",
"status": 422,
"details": [
{ "field": "ship_data.contact_phone", "issues": ["The contact phone field is required."] }
]
}
}code— מחרוזת מכונה יציבה. הסתעפו לפיה.status— משקף את קוד סטטוס ה-HTTP.details— נוכח רק כשיש מידע ברמת השדה; מושמט לחלוטין כשהוא ריק. עבורvalidation_failedזו רשימה של אובייקטי{ "field": "...", "issues": ["..."] }, אחד לכל שדה לא תקין.
כשלי שרת לא מטופלים מוצגים כ-code: "server_error" עם status: 500 והודעה גנרית ("An unexpected error occurred." בפרודקשן — הפרטים מוסתרים אלא אם מצב debug פעיל). חריגות HTTP אחרות שאינן 500 ושאינן ממופות לקוד ספציפי נופלות חזרה ל-code: "error".
שלב 2 — ספר ההפעלה לפי קוד
| HTTP | code | משמעות | מה לעשות |
|---|---|---|---|
| 401 | unauthenticated | X-Client-Id / X-Client-Secret חסרים/שגויים. | תקנו את פרטי הגישה. אל תנסו שוב ללא שינוי. |
| 403 | forbidden | מאומתים, אבל רישיון היעד אינו שלכם, אינו פעיל, פג תוקפו — או שלחשבון שלכם אין רישיון פעיל. | תקנו את ה-license_key (ראו מדריך רישיונות) או את הרישיון עצמו. אל תנסו שוב ללא שינוי. |
| 403 | package_limit_reached | מכסת המשלוחים של המנוי שלכם מוצתה. | שדרגו/חדשו את החבילה. ניסיון חוזר לא יעזור עד שהמכסה תשתנה. |
| 404 | not_found | המשאב אינו קיים או שייך למישהו אחר (UUIDs זרים מחזירים 404, לא 403). | בדקו את המזהה. אל תנסו שוב ללא שינוי. |
| 409 | duplicate_request | יצירה זהה מתרחשת ממש עכשיו — בקשה מקבילה מחזיקה במנעול היצירה. חולף. | המתינו רגע ונסו שוב את אותה בקשה; תקבלו את המשלוח שהושלם. |
| 409 | idempotency_key_conflict | השתמשתם שוב ב-Idempotency-Key עם גוף בקשה שונה. שגיאת לקוח קבועה — המפתח קשור לגוף של השימוש הראשון בו. | תקנו את יצירת המפתחות שלכם: משלוח לוגי חדש ⇐ מפתח חדש. לעולם אל תנסו שוב ללא שינוי. |
| 422 | validation_failed | גוף הבקשה נכשל בוולידציה; הבעיות לפי שדה נמצאות ב-details. | תקנו את השדות המפורטים ב-details. לעולם אל תנסו שוב ללא שינוי. |
| 424 | carrier_error | ShipOS קיבלה את הבקשה שלכם, אבל חברת השילוח דחתה או הכשילה אותה (ההודעה שלה נמצאת ב-message). | בצד חברת השילוח. לעיתים קרובות חולף — ניתן לנסות שוב עם אותו Idempotency-Key אחרי השהיה. אם זה נמשך, חברת השילוח דוחה את הנתונים עצמם (כתובת שגויה, שירות שאינו נתמך); תקנו והשתמשו במפתח חדש. |
| 5xx | server_error | כשל בלתי צפוי בצד שלנו. | נסו שוב עם backoff, תוך שימוש חוזר באותו Idempotency-Key. |
טיפ — duplicate_request לעומת idempotency_key_conflict
שניהם 409, אבל תפעולית הם הפכים. duplicate_request אומר "אותה יצירה קורית ממש עכשיו — המתינו ונסו שוב, תקבלו את התוצאה המקורית." idempotency_key_conflict אומר "שלחתם גוף שונה תחת מפתח ישן — זה לעולם לא יצליח עד שתתקנו את הלקוח שלכם." נסו שוב את הראשון; לעולם אל תנסו שוב את השני.
שלב 3 — ניסיונות חוזרים בטוחים ב-POST /shipments
יצירת משלוח היא קריאה לחברת שילוח, מחויבת בתשלום ובלתי הפיכה. נתיב היצירה בנוי כך שניסיונות חוזרים יהיו בטוחים (ראו מדריך אידמפוטנטיות):
- שלחו תמיד header של
Idempotency-Key— UUID טרי לכל משלוח לוגי. - על timeout או 5xx, נסו שוב עם אותו מפתח + אותו גוף. תוצאת חברת השילוח נרשמת בצד השרת לפני ששורת המשלוח נשמרת, כך שגם אם התהליך קרס באמצע בקשה אחרי שקריאת חברת השילוח הצליחה, הניסיון החוזר שלכם מחזיר את תוצאת חברת השילוח שנרשמה — חברת השילוח אינה נקראת או מחויבת שוב.
- על 409
duplicate_request, המתינו קצרות ונסו שוב את אותו מפתח + גוף — תוצאת הבקשה הראשונה תוחזר. - על 424
carrier_error, נסו שוב את אותו מפתח + גוף אחרי השהיה (שיהוקים אצל חברות שילוח הם דבר שכיח). שגיאות 424 מתמשכות אומרות שהנתונים עצמם נדחים — תקנו אותם והשתמשו במפתח חדש. - לעולם אל תנסו שוב
422,403או409 idempotency_key_conflictללא שינוי — הם ייכשלו באופן זהה לנצח.
const crypto = require('node:crypto');
const RETRYABLE_CODES = new Set(['duplicate_request', 'carrier_error', 'server_error']);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function createShipmentWithRetries(body, maxAttempts = 4) {
const idempotencyKey = crypto.randomUUID(); // מפתח אחד לכל משלוח לוגי
for (let attempt = 1; ; attempt++) {
let res;
try {
res = await fetch('https://app.shipos.co.il/api/v2/shipments', {
method: 'POST',
headers: {
'X-Client-Id': process.env.SHIPOS_CLIENT_ID,
'X-Client-Secret': process.env.SHIPOS_CLIENT_SECRET,
'Accept': 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(60_000),
});
} catch (err) {
// timeout / כשל תעבורה: התוצאה לא ידועה — נסו שוב עם אותו מפתח + גוף.
if (attempt >= maxAttempts) throw err;
await sleep(2 ** attempt * 1000);
continue;
}
if (res.ok) {
const { data } = await res.json(); // 201 (או המשלוח המקורי על מפתח שנוסה שוב)
return data;
}
const { error } = await res.json();
const retryable = res.status >= 500 || RETRYABLE_CODES.has(error.code);
if (!retryable || attempt >= maxAttempts) {
// 422 validation_failed, 403, idempotency_key_conflict, … — תקנו, אל תנסו שוב.
throw new Error(`Shipment create failed [${error.code}]: ${error.message}`);
}
await sleep(2 ** attempt * 1000); // exponential backoff, אותו מפתח + גוף
}
}<?php
// composer require guzzlehttp/guzzle ramsey/uuid
use GuzzleHttp\Exception\ConnectException;
const RETRYABLE_CODES = ['duplicate_request', 'carrier_error', 'server_error'];
function createShipmentWithRetries(array $body, int $maxAttempts = 4): array
{
$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.shipos.co.il/api/v2/',
'timeout' => 60,
'http_errors' => false,
]);
$idempotencyKey = \Ramsey\Uuid\Uuid::uuid4()->toString(); // מפתח אחד לכל משלוח לוגי
for ($attempt = 1; ; $attempt++) {
try {
$response = $client->post('shipments', [
'headers' => [
'X-Client-Id' => getenv('SHIPOS_CLIENT_ID'),
'X-Client-Secret' => getenv('SHIPOS_CLIENT_SECRET'),
'Accept' => 'application/json',
'Idempotency-Key' => $idempotencyKey, // אותו מפתח בכל ניסיון חוזר
],
'json' => $body,
]);
} catch (ConnectException $e) {
// timeout / כשל תעבורה: התוצאה לא ידועה — נסו שוב עם אותו מפתח + גוף.
if ($attempt >= $maxAttempts) {
throw $e;
}
sleep(2 ** $attempt);
continue;
}
$payload = json_decode($response->getBody()->getContents(), true);
if ($response->getStatusCode() < 300) {
return $payload['data']; // 201 (או המשלוח המקורי על מפתח שנוסה שוב)
}
$code = $payload['error']['code'] ?? 'error';
$retryable = $response->getStatusCode() >= 500 || in_array($code, RETRYABLE_CODES, true);
if (! $retryable || $attempt >= $maxAttempts) {
// 422 validation_failed, 403, idempotency_key_conflict, … — תקנו, אל תנסו שוב.
throw new \RuntimeException("Shipment create failed [{$code}]: ".($payload['error']['message'] ?? ''));
}
sleep(2 ** $attempt); // exponential backoff, אותו מפתח + גוף
}
}<?php
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
function createShipmentWithRetries(array $body, int $maxAttempts = 4): array
{
$idempotencyKey = (string) Str::uuid(); // מפתח אחד לכל משלוח לוגי
for ($attempt = 1; ; $attempt++) {
try {
$response = Http::withHeaders([
'X-Client-Id' => config('services.shipos.client_id'),
'X-Client-Secret' => config('services.shipos.client_secret'),
'Accept' => 'application/json',
'Idempotency-Key' => $idempotencyKey,
])->timeout(60)->post('https://app.shipos.co.il/api/v2/shipments', $body);
} catch (\Illuminate\Http\Client\ConnectionException $e) {
// timeout / כשל תעבורה: התוצאה לא ידועה — נסו שוב עם אותו מפתח + גוף.
if ($attempt >= $maxAttempts) {
throw $e;
}
sleep(2 ** $attempt);
continue;
}
if ($response->successful()) {
return $response->json('data'); // 201 (או המשלוח המקורי על מפתח שנוסה שוב)
}
$code = $response->json('error.code');
$retryable = $response->serverError() // 5xx server_error
|| $code === 'duplicate_request' // יצירה מקבילה בתהליך
|| $code === 'carrier_error'; // שיהוק אצל חברת השילוח — אותו מפתח, אחרי השהיה
if (! $retryable || $attempt >= $maxAttempts) {
// 422 validation_failed, 403, idempotency_key_conflict, … — תקנו, אל תנסו שוב.
throw new \RuntimeException("Shipment create failed [{$code}]: ".$response->json('error.message'));
}
sleep(2 ** $attempt); // exponential backoff, אותו מפתח + גוף
}
}# pip install httpx
import os
import time
import uuid
import httpx
RETRYABLE_CODES = {"duplicate_request", "carrier_error", "server_error"}
def create_shipment_with_retries(body: dict, max_attempts: int = 4) -> dict:
idempotency_key = str(uuid.uuid4()) # מפתח אחד לכל משלוח לוגי
headers = {
"X-Client-Id": os.environ["SHIPOS_CLIENT_ID"],
"X-Client-Secret": os.environ["SHIPOS_CLIENT_SECRET"],
"Accept": "application/json",
"Idempotency-Key": idempotency_key, # אותו מפתח בכל ניסיון חוזר
}
for attempt in range(1, max_attempts + 1):
try:
response = httpx.post(
"https://app.shipos.co.il/api/v2/shipments",
headers=headers,
json=body,
timeout=60,
)
except httpx.TransportError:
# timeout / כשל תעבורה: התוצאה לא ידועה — נסו שוב עם אותו מפתח + גוף.
if attempt == max_attempts:
raise
time.sleep(2**attempt)
continue
if response.is_success:
return response.json()["data"] # 201 (או המשלוח המקורי על מפתח שנוסה שוב)
error = response.json().get("error", {})
retryable = response.status_code >= 500 or error.get("code") in RETRYABLE_CODES
if not retryable or attempt == max_attempts:
# 422 validation_failed, 403, idempotency_key_conflict, … — תקנו, אל תנסו שוב.
raise RuntimeError(f"Shipment create failed [{error.get('code')}]: {error.get('message')}")
time.sleep(2**attempt) # exponential backoff, אותו מפתח + גוףpackage main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"github.com/google/uuid"
)
var retryableCodes = map[string]bool{
"duplicate_request": true, "carrier_error": true, "server_error": true,
}
func createShipmentWithRetries(body map[string]any, maxAttempts int) (map[string]any, error) {
raw, _ := json.Marshal(body)
idempotencyKey := uuid.NewString() // מפתח אחד לכל משלוח לוגי
client := &http.Client{Timeout: 60 * time.Second}
for attempt := 1; ; attempt++ {
req, _ := http.NewRequest("POST", "https://app.shipos.co.il/api/v2/shipments", bytes.NewReader(raw))
req.Header.Set("X-Client-Id", os.Getenv("SHIPOS_CLIENT_ID"))
req.Header.Set("X-Client-Secret", os.Getenv("SHIPOS_CLIENT_SECRET"))
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey) // אותו מפתח בכל ניסיון חוזר
res, err := client.Do(req)
if err != nil {
// timeout / כשל תעבורה: התוצאה לא ידועה — נסו שוב עם אותו מפתח + גוף.
if attempt >= maxAttempts {
return nil, err
}
time.Sleep(time.Duration(1<<attempt) * time.Second)
continue
}
var payload struct {
Data map[string]any `json:"data"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
json.NewDecoder(res.Body).Decode(&payload)
res.Body.Close()
if res.StatusCode < 300 {
return payload.Data, nil // 201 (או המשלוח המקורי על מפתח שנוסה שוב)
}
retryable := res.StatusCode >= 500 || retryableCodes[payload.Error.Code]
if !retryable || attempt >= maxAttempts {
// 422 validation_failed, 403, idempotency_key_conflict, … — תקנו, אל תנסו שוב.
return nil, fmt.Errorf("shipment create failed [%s]: %s", payload.Error.Code, payload.Error.Message)
}
time.Sleep(time.Duration(1<<attempt) * time.Second) // exponential backoff, אותו מפתח + גוף
}
}// Java 17+ — java.net.http + Jackson עבור קוד השגיאה
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Set;
import java.util.UUID;
public class SafeShipmentCreate {
static final Set<String> RETRYABLE_CODES =
Set.of("duplicate_request", "carrier_error", "server_error");
static String create(String jsonBody, int maxAttempts) throws Exception {
String idempotencyKey = UUID.randomUUID().toString(); // מפתח אחד לכל משלוח לוגי
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.shipos.co.il/api/v2/shipments"))
.header("X-Client-Id", System.getenv("SHIPOS_CLIENT_ID"))
.header("X-Client-Secret", System.getenv("SHIPOS_CLIENT_SECRET"))
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.header("Idempotency-Key", idempotencyKey) // אותו מפתח בכל ניסיון חוזר
.timeout(Duration.ofSeconds(60))
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpClient client = HttpClient.newHttpClient();
for (int attempt = 1; ; attempt++) {
HttpResponse<String> response;
try {
response = client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException e) {
// timeout / כשל תעבורה: התוצאה לא ידועה — נסו שוב עם אותו מפתח + גוף.
if (attempt >= maxAttempts) {
throw e;
}
Thread.sleep(1000L << attempt);
continue;
}
if (response.statusCode() < 300) {
return response.body(); // {"data":{...}} — המשלוח המקורי על מפתח שנוסה שוב
}
String code = new ObjectMapper().readTree(response.body())
.path("error").path("code").asText();
boolean retryable = response.statusCode() >= 500 || RETRYABLE_CODES.contains(code);
if (!retryable || attempt >= maxAttempts) {
// 422 validation_failed, 403, idempotency_key_conflict, … — תקנו, אל תנסו שוב.
throw new RuntimeException("Shipment create failed [" + code + "]: " + response.body());
}
Thread.sleep(1000L << attempt); // exponential backoff, אותו מפתח + גוף
}
}
}// .NET 8+ — System.Net.Http.Json
using System.Net.Http.Json;
using System.Text.Json;
var retryableCodes = new HashSet<string>
{
"duplicate_request", "carrier_error", "server_error",
};
async Task<JsonElement> CreateShipmentWithRetries(object body, int maxAttempts = 4)
{
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };
http.DefaultRequestHeaders.Add("X-Client-Id",
Environment.GetEnvironmentVariable("SHIPOS_CLIENT_ID"));
http.DefaultRequestHeaders.Add("X-Client-Secret",
Environment.GetEnvironmentVariable("SHIPOS_CLIENT_SECRET"));
// מפתח אחד לכל משלוח לוגי — אותו מפתח נשלח בכל ניסיון חוזר.
http.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
for (var attempt = 1; ; attempt++)
{
HttpResponseMessage response;
try
{
response = await http.PostAsJsonAsync(
"https://app.shipos.co.il/api/v2/shipments", body);
}
catch (Exception e) when (e is HttpRequestException or TaskCanceledException)
{
// timeout / כשל תעבורה: התוצאה לא ידועה — נסו שוב עם אותו מפתח + גוף.
if (attempt >= maxAttempts)
{
throw;
}
await Task.Delay(TimeSpan.FromSeconds(1 << attempt));
continue;
}
var payload = (await response.Content.ReadFromJsonAsync<JsonDocument>())!.RootElement;
if (response.IsSuccessStatusCode)
{
return payload.GetProperty("data"); // 201, או המשלוח המקורי על מפתח שנוסה שוב
}
var error = payload.GetProperty("error");
var code = error.GetProperty("code").GetString()!;
var retryable = (int) response.StatusCode >= 500 || retryableCodes.Contains(code);
if (!retryable || attempt >= maxAttempts)
{
// 422 validation_failed, 403, idempotency_key_conflict, … — תקנו, אל תנסו שוב.
throw new InvalidOperationException(
$"Shipment create failed [{code}]: {error.GetProperty("message").GetString()}");
}
await Task.Delay(TimeSpan.FromSeconds(1 << attempt)); // backoff, אותו מפתח + גוף
}
}require "json"
require "net/http"
require "securerandom"
RETRYABLE_CODES = %w[duplicate_request carrier_error server_error].freeze
def create_shipment_with_retries(body, max_attempts: 4)
uri = URI("https://app.shipos.co.il/api/v2/shipments")
request = Net::HTTP::Post.new(uri)
request["X-Client-Id"] = ENV.fetch("SHIPOS_CLIENT_ID")
request["X-Client-Secret"] = ENV.fetch("SHIPOS_CLIENT_SECRET")
request["Accept"] = "application/json"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = SecureRandom.uuid # מפתח אחד לכל משלוח לוגי, בשימוש חוזר בניסיונות חוזרים
request.body = JSON.generate(body)
attempt = 0
loop do
attempt += 1
begin
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 60) do |http|
http.request(request)
end
rescue IOError, SystemCallError, Timeout::Error => e
# timeout / כשל תעבורה: התוצאה לא ידועה — נסו שוב עם אותו מפתח + גוף.
raise e if attempt >= max_attempts
sleep(2**attempt)
next
end
payload = JSON.parse(response.body)
# 201 (או המשלוח המקורי על מפתח שנוסה שוב)
return payload.fetch("data") if response.is_a?(Net::HTTPSuccess)
code = payload.dig("error", "code")
retryable = response.code.to_i >= 500 || RETRYABLE_CODES.include?(code)
# 422 validation_failed, 403, idempotency_key_conflict, … — תקנו, אל תנסו שוב.
if !retryable || attempt >= max_attempts
raise "Shipment create failed [#{code}]: #{payload.dig("error", "message")}"
end
sleep(2**attempt) # exponential backoff, אותו מפתח + גוף
end
end// [dependencies]
// reqwest = { version = "0.12", features = ["json"] }
// tokio = { version = "1", features = ["full"] }
// serde_json = "1"
// uuid = { version = "1", features = ["v4"] }
use serde_json::Value;
use std::time::Duration;
const RETRYABLE_CODES: [&str; 3] = ["duplicate_request", "carrier_error", "server_error"];
async fn create_shipment_with_retries(
body: &Value,
max_attempts: u32,
) -> Result<Value, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let idempotency_key = uuid::Uuid::new_v4().to_string(); // מפתח אחד לכל משלוח לוגי
for attempt in 1..=max_attempts {
let sent = client
.post("https://app.shipos.co.il/api/v2/shipments")
.header("X-Client-Id", std::env::var("SHIPOS_CLIENT_ID")?)
.header("X-Client-Secret", std::env::var("SHIPOS_CLIENT_SECRET")?)
.header("Accept", "application/json")
.header("Idempotency-Key", &idempotency_key) // אותו מפתח בכל ניסיון חוזר
.timeout(Duration::from_secs(60))
.json(body)
.send()
.await;
let response = match sent {
Ok(response) => response,
// timeout / כשל תעבורה: התוצאה לא ידועה — נסו שוב עם אותו מפתח + גוף.
Err(err) => {
if attempt == max_attempts {
return Err(err.into());
}
tokio::time::sleep(Duration::from_secs(1 << attempt)).await;
continue;
}
};
let status = response.status();
let payload: Value = response.json().await?;
if status.is_success() {
return Ok(payload["data"].clone()); // 201, או המשלוח המקורי על מפתח שנוסה שוב
}
let code = payload["error"]["code"].as_str().unwrap_or("error");
let retryable = status.is_server_error() || RETRYABLE_CODES.contains(&code);
if !retryable || attempt == max_attempts {
// 422 validation_failed, 403, idempotency_key_conflict, … — תקנו, אל תנסו שוב.
return Err(format!(
"Shipment create failed [{code}]: {}",
payload["error"]["message"]
)
.into());
}
tokio::time::sleep(Duration::from_secs(1 << attempt)).await; // backoff, אותו מפתח + גוף
}
unreachable!()
}אזהרה — אותו מפתח פירושו אותו גוף
ה-Idempotency-Key קשור לגוף הבקשה המדויק של השימוש הראשון בו. נסו שוב עם הגוף הזהה בלבד. אם אתם צריכים לשנות משהו — כתובת, חבילות, סוג שירות — זה משלוח לוגי חדש: צרו מפתח חדש. שימוש חוזר במפתח הישן עם גוף שהשתנה מחזיר 409 idempotency_key_conflict בכל פעם.
ראו גם
- מדריך השגיאות — מפרט המעטפת המלא.
- מדריך אידמפוטנטיות — תחום המפתח, טביעת-האצבע של הגוף כשאין מפתח, וסמנטיקת התאוששות מקריסה.
- Shipments — הייחוס של
POST /shipments.