Skip to content

Commit 8ef0453

Browse files
committed
feat: pin the supported API contract on requests
1 parent 64a6895 commit 8ef0453

3 files changed

Lines changed: 59 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ Get a key at [parseapi.com](https://parseapi.com). `Client::from_env()` reads `P
1717

1818
## API versions
1919

20-
Choose your team's API version in [Dashboard → API version](https://parseapi.com/dashboard/versions). One setting applies to every key, including new and replacement keys. Existing teams keep `1.0.0`; new teams start on `2.0.0`. Keep the same keys and lookup URLs. Installing or upgrading the package does not change the team's setting.
20+
**Unreleased: planned for the next major SDK release.** This source candidate sends `Parse-Version: 2.0.0` on every request, including retries. Its response types match API `2.0.0`, and the client selects that contract automatically. No extra constructor setting or key change is needed. This behavior requires the matching API request-version release.
2121

22-
SDK `0.3.2` targets API `1.0.0`. SDK `0.4.0` and the examples and response types in this source tree target API `2.0.0`. Use a package release documented for your team's version. These types do not model every historical response; moving to `2.0.0` may require updating code that reads renamed, moved or removed fields.
22+
The team setting in [Dashboard API version](https://parseapi.com/dashboard/versions) is the default for requests without a version header. This SDK's header takes precedence without changing that saved default. Existing published packages keep their documented behavior.
2323

24-
Test the target contract in a separate development team before changing your production team's version. A change applies to every integration in that team. See [API versions and migration](https://parseapi.com/docs/versioning).
24+
Test the new SDK dependency in staging, then deploy the same locked dependency with your application code and existing production key. Future major SDK upgrades can deliberately select a newer API contract, so review their migration notes before upgrading. Rolling back the code and dependency restores the contract selected by that SDK release. If the older SDK does not send a version header, its requests use the team default, which must stay unchanged through that rollback window.
25+
26+
Keep the package version locked in your dependency configuration or lockfile. The selected API contract stays fixed across releases within this planned SDK major. See [API versions and migration](https://parseapi.com/docs/versioning).
2527

2628
## Weather from a postal code
2729

‎src/lib.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use std::fmt;
2020
use std::time::Duration;
2121

2222
const DEFAULT_BASE_URL: &str = "https://api.parseapi.com";
23+
// The response types' wire contract. Changes require a reviewed major SDK release.
24+
const API_VERSION: &str = "2.0.0";
2325
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
2426
const DEFAULT_RETRIES: u32 = 2;
2527
const RETRY_STATUS: [u16; 5] = [429, 500, 502, 503, 504];
@@ -1355,6 +1357,7 @@ impl Client {
13551357
.http
13561358
.get(&url)
13571359
.header("X-API-Key", &self.api_key)
1360+
.header("Parse-Version", API_VERSION)
13581361
.header(reqwest::header::USER_AGENT, ua.unwrap_or(USER_AGENT));
13591362
if !query.is_empty() {
13601363
request = request.query(&query);

‎tests/client.rs‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ async fn sends_key_and_user_agent() {
311311
let _ = client.country("US").await;
312312
let recorded = server.requests();
313313
assert_eq!(recorded[0].headers["x-api-key"], "test_key_123");
314+
assert_eq!(recorded[0].headers["parse-version"], "2.0.0");
314315
let ua = &recorded[0].headers["user-agent"];
315316
assert!(ua.starts_with("parseapi-rust/0."), "unexpected UA {ua}");
316317
}
@@ -324,6 +325,56 @@ async fn useragent_overrides_ua_header() {
324325
server.requests()[0].headers["user-agent"],
325326
"Mozilla/5.0 (Test)"
326327
);
328+
assert_eq!(server.requests()[0].headers["parse-version"], "2.0.0");
329+
assert_eq!(server.requests()[0].headers["x-api-key"], "test_key_123");
330+
}
331+
332+
#[tokio::test]
333+
async fn api_contract_pin_survives_retries_and_self_lookup() {
334+
let server = TestServer::start_with_headers(vec![
335+
(503, r#"{"code":"unavailable","message":"Try again"}"#, "Retry-After: 0\r\n".into()),
336+
(200, r#"{"ip":"192.0.2.1","country":null,"deep":{"datacenter":null},"future":true}"#, String::new()),
337+
]);
338+
let client = Client::builder()
339+
.api_key("test_key_123")
340+
.base_url(&server.base_url)
341+
.retries(1)
342+
.build()
343+
.unwrap();
344+
let result = client.ip_self(IpSelfOptions::default().deep(true)).await.unwrap();
345+
assert_eq!(result.ip, "192.0.2.1");
346+
assert_eq!(result.country, None);
347+
assert_eq!(result.deep.unwrap().datacenter, None);
348+
let requests = server.requests();
349+
assert_eq!(requests.len(), 2);
350+
for request in requests {
351+
assert_eq!(request.target, "/ip?deep=true");
352+
assert_eq!(request.headers["parse-version"], "2.0.0");
353+
assert_eq!(request.headers["x-api-key"], "test_key_123");
354+
assert_eq!(request.headers["user-agent"], concat!("parseapi-rust/", env!("CARGO_PKG_VERSION")));
355+
}
356+
}
357+
358+
#[tokio::test]
359+
async fn api_version_errors_do_not_retry_or_fall_back() {
360+
for status in [400, 410] {
361+
let server = TestServer::start(vec![(status, r#"{"code":"invalid_request","message":"Unsupported API version","request_id":"req_version"}"#)]);
362+
let client = Client::builder()
363+
.api_key("test_key_123")
364+
.base_url(&server.base_url)
365+
.retries(2)
366+
.build()
367+
.unwrap();
368+
let error = client.ip_self(None).await.unwrap_err();
369+
assert_eq!(error.status(), Some(status));
370+
assert_eq!(error.code(), Some("invalid_request"));
371+
match error {
372+
Error::Api { request_id, .. } => assert_eq!(request_id.as_deref(), Some("req_version")),
373+
other => panic!("expected API error, got {other}"),
374+
}
375+
assert_eq!(server.requests().len(), 1);
376+
assert_eq!(server.requests()[0].headers["parse-version"], "2.0.0");
377+
}
327378
}
328379

329380
#[test]

0 commit comments

Comments
 (0)