Skip to main content

GURT Client Library

The GURT client library (for Rust) provides a high-level, HTTP-like interface for making requests to GURT servers. It handles TLS encryption, protocol handshakes, and connection management automatically.

Bindings​

  • Godot by Gurted - 🔗 link
  • No bidings for other languages are currently available.

Installation​

Install via Cargo:

cargo add gurtlib

Quick Start​

use gurtlib::prelude::*;

#[tokio::main]
async fn main() -> Result<()> {
let client = GurtClient::new();

// Make a GET request
let response = client.get("gurt://example.com/").await?;

println!("Status: {}", response.status_code);
println!("Body: {}", response.text()?);

Ok(())
}

Creating a Client​

Default Client​

let client = GurtClient::new();

Custom Configuration​

use tokio::time::Duration;

let config = GurtClientConfig {
connect_timeout: Duration::from_secs(10),
request_timeout: Duration::from_secs(30),
handshake_timeout: Duration::from_secs(5),
user_agent: "MyApp/1.0.0".to_string(),
max_redirects: 5,
};

let client = GurtClient::with_config(config);

Making Requests​

GET Requests​

let response = client.get("gurt://api.example.com/users").await?;

if response.is_success() {
println!("Success: {}", response.text()?);
} else {
println!("Error: {} {}", response.status_code, response.status_message);
}

POST Requests​

Text Data​

let response = client.post("gurt://api.example.com/submit", "Hello, GURT!").await?;

JSON Data​

use serde_json::json;

let data = json!({
"name": "John Doe",
"email": "[email protected]"
});

let response = client.post_json("gurt://api.example.com/users", &data).await?;

PUT Requests​

// Text data
let response = client.put("gurt://api.example.com/resource/123", "Updated content").await?;

// JSON data
let update_data = json!({"status": "completed"});
let response = client.put_json("gurt://api.example.com/tasks/456", &update_data).await?;

DELETE Requests​

let response = client.delete("gurt://api.example.com/users/123").await?;

HEAD Requests​

let response = client.head("gurt://api.example.com/large-file").await?;

// Check headers without downloading body
let content_length = response.headers.get("content-length");

OPTIONS Requests​

let response = client.options("gurt://api.example.com/endpoint").await?;

// Check allowed methods
let allowed_methods = response.headers.get("allow");

PATCH Requests​

let patch_data = json!({"name": "Updated Name"});
let response = client.patch_json("gurt://api.example.com/users/123", &patch_data).await?;

Response Handling​

Response Structure​

pub struct GurtResponse {
pub version: String,
pub status_code: u16,
pub status_message: String,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
}

Accessing Response Data​

let response = client.get("gurt://api.example.com/data").await?;

// Status information
println!("Status Code: {}", response.status_code);
println!("Status Message: {}", response.status_message);

// Headers
for (name, value) in &response.headers {
println!("{}: {}", name, value);
}

// Body as string
let text = response.text()?;

// Body as bytes
let bytes = &response.body;

// Parse JSON response
let json_data: serde_json::Value = serde_json::from_slice(&response.body)?;

Status Code Checking​

if response.is_success() {
// 2xx status codes
println!("Request successful");
} else if response.is_client_error() {
// 4xx status codes
println!("Client error: {}", response.status_message);
} else if response.is_server_error() {
// 5xx status codes
println!("Server error: {}", response.status_message);
}

Protocol Implementation​

The GURT client automatically handles the complete GURT protocol:

  1. TCP Connection: Establishes initial connection to the server
  2. Handshake: Sends HANDSHAKE request and waits for 101 Switching Protocols
  3. TLS Upgrade: Upgrades the connection to TLS 1.3 with GURT ALPN
  4. Request/Response: Sends the actual HTTP-style request over encrypted connection

All of this happens transparently when you call methods like get(), post(), etc.

URL Parsing​

The client automatically parses gurt:// URLs:

// These are all valid GURT URLs:
client.get("gurt://example.com/").await?; // Port 4878 (default)
client.get("gurt://example.com:8080/api").await?; // Custom port
client.get("gurt://192.168.1.100/test").await?; // IP address
client.get("gurt://localhost:4878/dev").await?; // Localhost

URL Components​

The client extracts:

  • Host: Domain name or IP address
  • Port: Specified port or default (4878)
  • Path: Request path (defaults to /)

Error Handling​

Error Types​

use gurtlib::GurtError;

match client.get("gurt://invalid-url").await {
Ok(response) => {
// Handle successful response
}
Err(GurtError::InvalidMessage(msg)) => {
println!("Invalid request: {}", msg);
}
Err(GurtError::Connection(msg)) => {
println!("Connection error: {}", msg);
}
Err(GurtError::Timeout(msg)) => {
println!("Request timeout: {}", msg);
}
Err(GurtError::Io(err)) => {
println!("IO error: {}", err);
}
Err(err) => {
println!("Other error: {}", err);
}
}

Timeout Configuration​

let config = GurtClientConfig {
connect_timeout: Duration::from_secs(5), // Connection timeout
request_timeout: Duration::from_secs(30), // Overall request timeout
handshake_timeout: Duration::from_secs(3), // GURT handshake timeout
..Default::default()
};

Why Rust-first?​

Rust was chosen for the official GURT protocol implementation due to its embedded nature.

To keep the core organized & not write identical code in GDScript, we used a GDExtension. A GDExtension can be created with a multitude of languages, but Rust was the one that provided the best performance, size, and programming ergonomics.

We expect the community to implement bindings for other languages, such as Python and JavaScript, to make GURT accessible for everybody!

Example: Building a GURT API Client​

use gurtlib::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize)]
struct CreateUser {
name: String,
email: String,
}

#[derive(Deserialize)]
struct User {
id: u64,
name: String,
email: String,
}

struct ApiClient {
client: GurtClient,
base_url: String,
}

impl ApiClient {
fn new(base_url: String) -> Self {
Self {
client: GurtClient::new(),
base_url,
}
}

async fn create_user(&self, user: CreateUser) -> Result<User> {
let url = format!("{}/users", self.base_url);
let response = self.client.post_json(&url, &user).await?;

if !response.is_success() {
return Err(GurtError::invalid_message(
format!("API error: {}", response.status_message)
));
}

let user: User = serde_json::from_slice(&response.body)?;
Ok(user)
}

async fn get_user(&self, id: u64) -> Result<User> {
let url = format!("{}/users/{}", self.base_url, id);
let response = self.client.get(&url).await?;

if response.status_code == 404 {
return Err(GurtError::invalid_message("User not found".to_string()));
}

if !response.is_success() {
return Err(GurtError::invalid_message(
format!("API error: {}", response.status_message)
));
}

let user: User = serde_json::from_slice(&response.body)?;
Ok(user)
}
}

#[tokio::main]
async fn main() -> Result<()> {
let api = ApiClient::new("gurt://api.example.com".to_string());

// Create a user
let new_user = CreateUser {
name: "Alice".to_string(),
email: "[email protected]".to_string(),
};

let user = api.create_user(new_user).await?;
println!("Created user: {} (ID: {})", user.name, user.id);

// Retrieve the user
let retrieved_user = api.get_user(user.id).await?;
println!("Retrieved user: {}", retrieved_user.name);

Ok(())
}