Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions crates/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,33 @@ Environment variables (prefix `QN_SDK__`, separator `__`):
| `QN_SDK__STREAMS__BASE_URL` | no | `https://api.quicknode.com/streams/rest/v1/` | Override streams base URL |
| `QN_SDK__WEBHOOKS__BASE_URL` | no | `https://api.quicknode.com/webhooks/rest/v1/` | Override webhooks base URL |
| `QN_SDK__KVSTORE__BASE_URL` | no | `https://api.quicknode.com/kv/rest/v1/` | Override KV store base URL |
| `QN_SDK__HTTP__HEADERS__<NAME>` | no | — | Custom HTTP header sent on every request. Overrides SDK-managed headers (see below). |

### Custom headers and `User-Agent`

Every outbound HTTP request includes an auto-generated `User-Agent` of the form:

```
quicknode-sdk-<language>/<sdk-version> (<os>-<arch>; <language>-<runtime-version>)
```

You can attach arbitrary headers via `HttpConfig.headers`. **These headers OVERRIDE any SDK-managed header with the same name**, including `User-Agent`, `x-api-key`, `Accept`, and `Content-Type`. Use this to inject correlation IDs, proxy auth, or to replace the default `User-Agent`. Header names are matched case-insensitively.

```rust
use std::collections::HashMap;
use quicknode_sdk::{HttpConfig, QuicknodeSdk, SdkFullConfig};

let mut headers = HashMap::new();
headers.insert("X-Correlation-Id".to_string(), "abc-123".to_string());
headers.insert("User-Agent".to_string(), "my-app/1.0".to_string()); // overrides SDK default

let qn = QuicknodeSdk::new(
&SdkFullConfig::builder()
.api_key("your-key")
.http(HttpConfig { headers: Some(headers), ..Default::default() })
.build(),
)?;
```

## Platform Support

Expand Down
29 changes: 29 additions & 0 deletions crates/core/examples/admin_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -857,13 +857,42 @@ async fn main() {
other => eprintln!("expected Api 404 from delete_rate_limit_override, got {other:?}"),
}

// Custom headers smoke test — construct an SDK that overrides User-Agent
// and adds a correlation header. We don't verify what reaches the wire
// here; we just confirm the config is accepted and an API call succeeds.
let mut custom_headers = std::collections::HashMap::new();
custom_headers.insert("User-Agent".to_string(), "qn-e2e-rust/1.0".to_string());
custom_headers.insert("X-E2E-Correlation".to_string(), "rust-smoke".to_string());
let with_headers = SdkFullConfig {
api_key: config.api_key.clone(),
http: Some(HttpConfig {
timeout_secs: None,
pool_max_idle_per_host: None,
headers: Some(custom_headers),
}),
admin: config.admin.clone(),
streams: None,
webhooks: None,
kvstore: None,
};
let headered = QuicknodeSdk::new(&with_headers).expect("build sdk with custom headers");
match headered
.admin
.get_endpoints(&GetEndpointsRequest::builder().limit(1).build())
.await
{
Ok(_) => println!("custom-headers smoke: ok"),
Err(e) => eprintln!("custom-headers smoke error: {e}"),
}

// 2) Timeout path — unreachable base URL + 1s timeout forces a timeout
// from reqwest, which maps to SdkError::Http with http_kind() == Timeout.
let blackhole = SdkFullConfig {
api_key: config.api_key.clone(),
http: Some(HttpConfig {
timeout_secs: Some(1),
pool_max_idle_per_host: None,
headers: None,
}),
admin: Some(AdminConfig {
base_url: Some("http://10.255.255.1/".to_string()),
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3667,6 +3667,7 @@ mod tests {
http: Some(HttpConfig {
timeout_secs: Some(-1),
pool_max_idle_per_host: None,
headers: None,
}),
admin: None,
streams: None,
Expand Down
50 changes: 48 additions & 2 deletions crates/core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,49 @@ use crate::errors::SdkError;
pub struct HttpConfig {
pub timeout_secs: Option<i64>,
pub pool_max_idle_per_host: Option<i32>,
/// Custom HTTP headers added to every outbound request.
///
/// **These headers OVERRIDE any SDK-managed header with the same name**,
/// including `User-Agent`, `x-api-key`, `Accept`, and `Content-Type`.
/// Header names are matched case-insensitively. Use this to override the
/// auto-generated User-Agent or inject correlation IDs, proxy auth, etc.
pub headers: Option<std::collections::HashMap<String, String>>,
}

#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl HttpConfig {
#[new]
#[pyo3(signature = (timeout_secs=None, pool_max_idle_per_host=None))]
pub fn new(timeout_secs: Option<i64>, pool_max_idle_per_host: Option<i32>) -> Self {
#[pyo3(signature = (timeout_secs=None, pool_max_idle_per_host=None, headers=None))]
pub fn new(
timeout_secs: Option<i64>,
pool_max_idle_per_host: Option<i32>,
headers: Option<std::collections::HashMap<String, String>>,
) -> Self {
HttpConfig {
timeout_secs,
pool_max_idle_per_host,
headers,
}
}
}

/// Identifies the language and runtime making SDK calls. Each binding crate
/// (Python, Node, Ruby) constructs this and passes it through
/// [`SdkConfig::new_with_client_info`] so the SDK's auto-generated
/// `User-Agent` reflects the actual caller, not the underlying Rust core.
#[derive(Debug, Clone)]
pub struct ClientInfo {
/// Short language identifier, e.g. `"python"`, `"node"`, `"ruby"`, `"rust"`.
pub language: String,
/// Runtime version of the language, e.g. `"3.12.4"`, `"20.10.0"`, `"3.3.0"`.
pub language_version: String,
/// Version string of the language-specific SDK package — read from the
/// language's own manifest (PyPI version, npm version, gem version).
pub sdk_version: String,
}

#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
Expand Down Expand Up @@ -238,4 +265,23 @@ mod tests {
Err(SdkError::Config(_))
));
}

#[test]
fn from_env_headers_round_trip() {
let cfg = build_config(&[
("api_key", "k"),
("http.headers.x-correlation-id", "abc"),
("http.headers.user-agent", "custom-ua/1.0"),
]);
let config = SdkFullConfig::from_config(cfg).unwrap();
let headers = config.http.unwrap().headers.unwrap();
assert_eq!(
headers.get("x-correlation-id").map(String::as_str),
Some("abc")
);
assert_eq!(
headers.get("user-agent").map(String::as_str),
Some("custom-ua/1.0")
);
}
}
Loading
Loading