This document describes the HTTP action architecture used in this project. Follow this pattern when creating new HTTP endpoints or building similar projects.
HTTP actions are organized using a controller-based architecture where each action is:
- A self-contained struct with its own route, input model, and handler
- Registered through a centralized builder
- Automatically documented via Swagger/OpenAPI through macro annotations
- Separated from business logic (which lives in
scripts/)
src/
├── http/
│ ├── mod.rs # HTTP module exports
│ ├── builder.rs # Controller registration
│ ├── start_up.rs # HTTP server initialization
│ ├── errors.rs # HTTP error types (if needed)
│ └── controllers/
│ ├── mod.rs # Controller module exports
│ └── {controller_group}/
│ ├── mod.rs # Group module exports
│ └── {action_name}_action.rs # Individual action
├── scripts/ # Business logic (called by actions)
└── app/
└── app_ctx.rs # Application context
Each HTTP action follows this pattern:
use std::sync::Arc;
use my_http_server::macros::*;
use my_http_server::*;
use crate::app::AppContext;
#[http_route(
method: "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS",
route: "/api/{controller}/v1/{action-name}",
deprecated_routes: ["/api/old-route"], // Optional: legacy routes that still work
summary: "Brief summary",
description: "Detailed description",
controller: "ControllerName",
input_data: "InputModelName",
authorized: Yes | No | YesWithClaims(["claim1", "claim2"]), // Optional: authorization config
result: [
{status_code: 200, description: "Success description", model: "OptionalModel"},
{status_code: 404, description: "Not found description"},
{status_code: 500, description: "Error description"},
]
)]
pub struct ActionName {
_app: Arc<AppContext>,
}
impl ActionName {
pub fn new(app: Arc<AppContext>) -> Self {
Self { _app: app }
}
}
async fn handle_request(
_action: &ActionName,
input_data: InputModelName,
_ctx: &HttpContext,
) -> Result<HttpOkResult, HttpFailResult> {
// Call business logic from scripts/
let result = crate::scripts::business_function(input_data.field).await;
match result {
Ok(output) => {
// Return success response
HttpOutput::as_json(output_model).into_ok_result(true).into()
// OR for text:
// HttpOutput::as_text(output).into_ok_result(true).into()
}
Err(error) => {
// Handle different error types
if error.contains("not found") {
return HttpFailResult::as_not_found(error, false).into_err();
}
return HttpFailResult::as_fatal_error(error).into_err();
}
}
}Important Notes:
- All fields in the
http_routemacro must be separated by commas (including the last field beforesummary,description, etc.) - When using
model:in theresult:field, the model MUST deriveMyHttpObjectStructure(see Section 4 for details)
Input models use the MyHttpInput derive macro and specify where data comes from. You can mix different input sources in a single model.
Available Input Sources:
- Query Parameters (
#[http_query]) - For GET requests and URL query strings - Path Parameters (
#[http_path]) - For route path variables like/api/users/{id} - HTTP Headers (
#[http_header]) - For reading HTTP headers - Body Data (
#[http_body]) - For JSON body in POST/PUT requests - Form Data (
#[http_form_data]) - For multipart/form-data requests - Raw Body (
#[http_body_raw]) - For raw body content (only one field allowed)
For POST/PUT requests (body data):
#[derive(MyHttpInput)]
pub struct AddDomainInputModel {
#[http_body(name = "domain", description = "Domain name to add certificate for")]
pub domain: String,
#[http_body(name = "email", description = "Email address for certificate registration")]
pub email: String,
}For GET requests (query parameters):
#[derive(MyHttpInput)]
pub struct GetCertInfoInputModel {
#[http_query(name = "domain", description = "Domain name")]
pub domain: String,
}Path Parameters:
#[derive(MyHttpInput)]
pub struct GetUserInputModel {
#[http_path(name = "id", description = "User ID")]
pub id: String,
#[http_query(name = "include_details", description = "Include user details", default = false)]
pub include_details: bool,
}HTTP Headers:
#[derive(MyHttpInput)]
pub struct ApiKeyInputModel {
#[http_header(name = "X-API-Key", description = "API key for authentication")]
pub api_key: String,
#[http_header(name = "X-Request-ID", description = "Request ID for tracking", default = "")]
pub request_id: Option<String>,
}Form Data (multipart/form-data):
#[derive(MyHttpInput)]
pub struct UploadFileInputModel {
#[http_form_data(name = "file", description = "File to upload")]
pub file: Vec<u8>, // File content
#[http_form_data(name = "description", description = "File description")]
pub description: String,
}File Uploads with FileContent:
For file uploads, you can use FileContent to access file metadata (filename, content type) along with the content:
use my_http_server::FileContent;
#[derive(MyHttpInput)]
pub struct UploadFileWithMetadataInputModel {
#[http_form_data(name = "file", description = "File to upload")]
pub file: FileContent, // Contains file_name, content_type, and content
#[http_form_data(name = "description", description = "File description")]
pub description: String,
}
async fn handle_request(
_action: &ActionName,
input_data: UploadFileWithMetadataInputModel,
_ctx: &HttpContext,
) -> Result<HttpOkResult, HttpFailResult> {
// Access file metadata
let file_name = input_data.file.file_name;
let content_type = input_data.file.content_type;
let content = input_data.file.content;
// Process the file...
HttpOutput::as_json(result).into_ok_result(true).into()
}FileContent Structure:
pub struct FileContent {
pub content_type: String, // MIME type (e.g., "image/png", "application/pdf")
pub file_name: String, // Original filename
pub content: Vec<u8>, // File content as bytes
}Note: FileContent can only be used with #[http_form_data] attributes and only works with actual file uploads in multipart/form-data requests. It cannot be used with query parameters, headers, or JSON body data.
Raw Body:
#[derive(MyHttpInput)]
pub struct RawDataInputModel {
#[http_body_raw(description = "Raw request body")]
pub content: Vec<u8>,
}Field Options:
All input field attributes support these optional parameters:
name- Parameter name (defaults to field name if not specified)description- Description for Swagger documentationdefault- Default value if parameter is missing (e.g.,default = "value",default = 0,default = false)validator- Custom validator function name (must be in scope)to_lowercase- Convert value to lowercase before parsingto_uppercase- Convert value to uppercase before parsingtrim- Trim whitespace before parsingprint_request_to_console- Debug flag to print request details
Optional Fields:
Fields can be optional by using Option<T>:
#[derive(MyHttpInput)]
pub struct SearchInputModel {
#[http_query(name = "query", description = "Search query")]
pub query: String,
#[http_query(name = "limit", description = "Result limit", default = 10)]
pub limit: Option<u32>, // Optional field
}Note: You cannot mix http_body, http_form_data, and http_body_raw in the same model - only one body type is allowed per input model.
Note on Field Transformations: The to_lowercase and to_uppercase attributes work only with String types, not with other types like Option<String> or numeric types.
model: in Result Field
When you specify model: YourResponseModel in the result: field of the http_route macro, the response model MUST derive MyHttpObjectStructure in addition to Serialize and Deserialize.
Required Pattern:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, MyHttpObjectStructure)]
pub struct YourResponseModel {
pub field1: String,
pub field2: i32,
}In the http_route macro:
#[http_route(
// ... other fields ...
result: [
{status_code: 200, description: "Ok response", model: YourResponseModel},
]
)]Why This Is Required:
The model: field in the result tells the framework to generate Swagger/OpenAPI documentation for your response model. The framework needs MyHttpObjectStructure to introspect the model structure and generate the proper schema documentation.
What Happens If You Forget:
If you use model: without deriving MyHttpObjectStructure, you will get a compilation error:
error[E0599]: no function or associated item named `get_http_data_structure` found for struct `YourResponseModel` in the current scope
Solution: Add MyHttpObjectStructure to your response model's derive macro.
When You DON'T Need MyHttpObjectStructure:
If you don't use model: in the result field, you can omit MyHttpObjectStructure:
// Simple response without model specification
#[derive(Serialize)]
struct SimpleResponse {
message: String,
}
// In http_route macro - no model: field
result: [
{status_code: 200, description: "Ok response"},
]
// In handler - still works fine
HttpOutput::as_json(response).into_ok_result(false)Complete Example:
use serde::{Deserialize, Serialize};
// Response model with MyHttpObjectStructure (required when using model:)
#[derive(Serialize, Deserialize, MyHttpObjectStructure)]
pub struct CertificateInfoHttpModel {
pub cn: String,
pub expires: String,
}
#[http_route(
method: "GET",
route: "/api/certificates/info",
input_data: GetCertInfoInput,
controller: "Certificates",
summary: "Get certificate information",
description: "Returns certificate details",
result: [
{status_code: 200, description: "Certificate info", model: CertificateInfoHttpModel},
]
)]
pub struct GetCertInfoAction {
// ...
}Summary:
| Scenario | Required Derives |
|---|---|
Using model: in result |
Serialize, Deserialize, MyHttpObjectStructure |
Not using model: in result |
Serialize (or Serialize, Deserialize if needed) |
The framework supports multiple response types through HttpOutput:
JSON Response:
HttpOutput::as_json(result_model).into_ok_result(true).into()Text Response:
HttpOutput::as_text(output_string).into_ok_result(true).into()HTML Response:
HttpOutput::as_html(html_content).into_ok_result(true).into()YAML Response:
HttpOutput::as_yaml(result_model).into_ok_result(true).into()Empty Response (204 No Content):
HttpOutput::Empty.into_ok_result(true).into()File Download:
HttpOutput::as_file(
"filename.txt".to_string(),
file_content_bytes
).into_ok_result(true).into()Redirect Response:
// Permanent redirect (301)
HttpOutput::as_redirect("https://example.com/new-url".to_string(), true)
.into_ok_result(true).into()
// Temporary redirect (302)
HttpOutput::as_redirect("https://example.com/temp-url".to_string(), false)
.into_ok_result(true).into()Custom Status Code and Headers:
HttpOutput::from_builder()
.with_status_code(201)
.with_header("Location", "/api/resource/123")
.with_content_type(WebContentType::Json)
.with_cookie(cookie)
.with_content(json_bytes)
.build()
.into_ok_result(true).into()Streaming Response:
let (output, producer) = HttpOutput::as_stream(100);
// Send output in handle_request
// Use producer to send chunks asynchronously
output.into_ok_result(true).into()Use appropriate error types based on the failure:
// Not Found (404)
HttpFailResult::as_not_found(error_message, false).into_err()
// Fatal Error (500)
HttpFailResult::as_fatal_error(error_message).into_err()
// Bad Request (400)
HttpFailResult::as_bad_request(error_message, false).into_err()
// Unauthorized (401) - Authentication required
HttpFailResult::as_unauthorized(Some("Authentication required")).into_err()
// Forbidden (403) - Not authorized
HttpFailResult::as_forbidden(error_message, false).into_err()
// Conflict (409)
HttpFailResult::as_conflict(error_message, false).into_err()
// Unprocessable Entity (422)
HttpFailResult::as_unprocessable_entity(error_message, false).into_err()Error Parameters:
Most error methods take two parameters:
error_message- The error message to returnwrite_log- Boolean indicating whether to write to log (usuallyfalsefor client errors,truefor server errors)
Custom Error with Status Code:
HttpFailResult::new(
HttpOutput::as_text("Custom error message"),
false, // write_log
false // write_telemetry
).into_err()Actions are registered in src/http/builder.rs:
use std::sync::Arc;
use my_http_server::controllers::{
ControllersMiddleware,
ControllersAuthorization,
RequiredClaims,
AuthErrorFactory
};
use crate::app::AppContext;
pub fn build_controllers(app: &Arc<AppContext>) -> ControllersMiddleware {
// Create middleware with optional authorization
let authorization = ControllersAuthorization::BearerAuthentication {
global: true, // Enable global authorization
global_claims: RequiredClaims::from_slice_of_str(&["admin", "user"]),
};
let auth_error_factory: Option<Arc<dyn AuthErrorFactory + Send + Sync>> =
Some(Arc::new(crate::http::MyAuthErrorFactory::new()));
let mut result = ControllersMiddleware::new(
Some(authorization), // Global authorization config
auth_error_factory // Custom error factory for auth failures
);
// Register POST actions
result.register_post_action(Arc::new(
crate::http::controllers::controller_group::ActionName::new(app.clone()),
));
// Register GET actions
result.register_get_action(Arc::new(
crate::http::controllers::controller_group::ActionName::new(app.clone()),
));
// Register PUT actions
result.register_put_action(Arc::new(
crate::http::controllers::controller_group::UpdateAction::new(app.clone()),
));
// Register DELETE actions
result.register_delete_action(Arc::new(
crate::http::controllers::controller_group::DeleteAction::new(app.clone()),
));
// Register OPTIONS actions (for CORS preflight)
result.register_options_action(Arc::new(
crate::http::controllers::controller_group::OptionsAction::new(app.clone()),
));
result
}Authorization Types:
The framework supports three authorization types:
BasicAuthentication- HTTP Basic AuthApiKeys- API key-based authenticationBearerAuthentication- Bearer token (JWT) authentication
Authorization Levels:
In the http_route macro, you can specify:
authorized: Yes- Requires authentication (uses global claims)authorized: No- No authentication required (public endpoint)authorized: YesWithClaims(["claim1", "claim2"])- Requires specific claims- Omit
authorized- Uses global authorization setting
Deprecated Routes:
Actions can support deprecated routes for backward compatibility:
#[http_route(
method: "GET",
route: "/api/v2/users/{id}",
deprecated_routes: ["/api/v1/users/{id}", "/api/users/{id}"],
// ... other parameters
)]All deprecated routes will still work but may be marked as deprecated in Swagger documentation.
Controller group module (controllers/{group}/mod.rs):
pub mod action_name_action;
pub use action_name_action::*;Main controllers module (controllers/mod.rs):
pub mod controller_group;HTTP server is initialized in src/http/start_up.rs:
use std::{net::SocketAddr, sync::Arc};
use my_http_server::controllers::swagger::SwaggerMiddleware;
use my_http_server::MyHttpServer;
use crate::app::AppContext;
pub fn start(app: &Arc<AppContext>) {
let mut http_server = MyHttpServer::new(SocketAddr::from(([0, 0, 0, 0], 8000)));
let controllers = Arc::new(super::builder::build_controllers(&app));
let swagger_middleware = SwaggerMiddleware::new(
controllers.clone(),
crate::app::APP_NAME.to_string(),
crate::app::APP_VERSION.to_string(),
);
http_server.add_middleware(Arc::new(swagger_middleware));
http_server.add_middleware(controllers);
http_server.start(app.app_states.clone(), my_logger::LOGGER.clone());
}- Separation of Concerns: HTTP actions are thin wrappers that delegate to business logic in
scripts/ - Type Safety: Use strongly-typed input/output models
- Documentation: All routes are auto-documented via
http_routemacro - Consistency: Follow the same pattern for all actions
- Error Handling: Use appropriate HTTP status codes and error types
- Modularity: Group related actions in controller modules
- Create the action file:
src/http/controllers/{group}/{action_name}_action.rs - Define the action struct with
#[http_route]macro - Create input model with
#[derive(MyHttpInput)] - Create output model (if returning JSON):
- If using
model:in result:Serialize,Deserialize,MyHttpObjectStructure - If NOT using
model:in result:Serialize(orSerialize, Deserializeif needed)
- If using
- Implement
handle_requestfunction that calls business logic - Export in module: Add to
{group}/mod.rs - Register in builder: Add registration call in
builder.rs
Cause: You're using model: YourModel in the result: field of http_route macro, but YourModel doesn't derive MyHttpObjectStructure.
Solution: Add MyHttpObjectStructure to your response model's derive macro:
// Before (incorrect)
#[derive(Serialize, Deserialize)]
struct MyResponse {
field: String,
}
// After (correct)
#[derive(Serialize, Deserialize, MyHttpObjectStructure)]
struct MyResponse {
field: String,
}Alternative Solution: If you don't need Swagger documentation for the response model, remove model: from the result field:
// In http_route macro
result: [
{status_code: 200, description: "Ok response"}, // No model: field
]Cause: Missing comma between fields in the http_route macro attributes.
Solution: Ensure all fields are separated by commas:
// Incorrect
controller: "ControllerName"
summary: "Summary",
// Correct
controller: "ControllerName",
summary: "Summary",Cause: The MyHttpObjectStructure trait is not in scope.
Solution: Ensure you have service_sdk::macros::use_my_http_server!(); at the top of your file, which brings MyHttpObjectStructure into scope.
See src/http/controllers/certbot/add_domain_action.rs for a complete POST action example.
See src/http/controllers/certificates/get_cert_info_action.rs for a complete GET action example.
This pattern requires:
my_http_servercrate with macros supportserdefor serializationtokiofor async runtime- Application context (
AppContext) for shared state
Routes can include path parameters using {param_name} syntax:
#[http_route(
method: "GET",
route: "/api/users/{userId}/posts/{postId}",
// ...
)]The corresponding input model must have matching #[http_path] fields:
#[derive(MyHttpInput)]
pub struct GetPostInputModel {
#[http_path(name = "userId", description = "User ID")]
pub user_id: String,
#[http_path(name = "postId", description = "Post ID")]
pub post_id: String,
}Input models can define alternative route patterns through the get_model_routes() function (automatically generated). This allows the same action to handle multiple route patterns that map to the same input model structure.
You can add custom validators to input fields. Validators can access the HTTP context for more complex validation:
#[derive(MyHttpInput)]
pub struct CreateUserInputModel {
#[http_body(
name = "email",
description = "Email address",
validator = "validate_email"
)]
pub email: String,
}
// Simple validator (value only)
fn validate_email(value: &str) -> Result<(), String> {
if value.contains('@') {
Ok(())
} else {
Err("Invalid email format".to_string())
}
}
// Validator with HTTP context access
fn validate_email_with_context(ctx: &HttpContext, value: &str) -> Result<(), HttpFailResult> {
// Can access request headers, path, etc. from ctx
if value.contains('@') {
Ok(())
} else {
Err(HttpFailResult::as_validation_error(
"Invalid email format".to_string()
))
}
}Validator Signatures:
- Simple:
fn validator_name(value: &str) -> Result<(), String> - With context:
fn validator_name(ctx: &HttpContext, value: &str) -> Result<(), HttpFailResult>
Add #[debug] attribute to a field or use print_request_to_console flag to debug request parsing:
#[derive(MyHttpInput)]
pub struct DebugInputModel {
#[http_query(
name = "test",
description = "Test parameter",
print_request_to_console
)]
pub test: String,
}The framework supports OPTIONS method for CORS preflight requests. Register OPTIONS actions the same way as other HTTP methods:
#[http_route(
method: "OPTIONS",
route: "/api/cors-endpoint",
// ...
)]The framework supports string and integer enums for input models using MyHttpStringEnum and MyHttpIntegerEnum:
String Enum:
#[derive(Clone, Copy, MyHttpStringEnum)]
pub enum DataSynchronizationPeriod {
#[http_enum_case(id = "0", value = "i", description = "Immediately Persist")]
Immediately,
#[http_enum_case(id = "1", value = "1", description = "Persist during 1 sec")]
Sec1,
#[http_enum_case(id = "5", value = "5", description = "Persist during 5 sec", default)]
Sec5,
#[http_enum_case(id = "15", value = "15", description = "Persist during 15 sec")]
Sec15,
}
#[derive(MyHttpInput)]
pub struct SyncInputModel {
#[http_query(name = "syncPeriod", description = "Synchronization period", default = "Sec5")]
pub sync_period: DataSynchronizationPeriod,
}Integer Enum:
#[derive(Clone, Copy, MyHttpIntegerEnum)]
pub enum StatusCode {
#[http_enum_case(id = "200", description = "OK")]
Ok,
#[http_enum_case(id = "404", description = "Not Found")]
NotFound,
}Enum Case Attributes:
id- Numeric identifier for the enum case (required)value- String value used in HTTP requests (optional, defaults to variant name)description- Description for Swagger documentation (required)default- Marks this case as the default value (optional)
You can create custom input field types based on String with additional validation and processing. This is useful for fields like passwords, emails, or other types that need special handling:
#[http_input_field(open_api_type: "Password")]
pub struct PasswordField(String);
fn process_value(src: &str) -> Result<rust_extensions::StrOrString, HttpFailResult> {
// Password validation
if src.len() < 8 {
return Err(HttpFailResult::as_validation_error(
"Password must be at least 8 characters long".to_string(),
));
}
let src = src.trim();
let src = src.to_lowercase();
Ok(rust_extensions::StrOrString::create_as_string(src))
}Usage in Input Models:
#[derive(MyHttpInput)]
pub struct AuthenticateInputModel {
#[http_form_data(description = "Email of user")]
pub email: String,
#[http_form_data(description = "Password of user")]
pub password: PasswordField, // Custom field type
}OpenAPI Types:
String(default)Password- Renders as password input in Swagger UI
You can set cookies in responses using CookieJar:
use my_http_server::cookies::{Cookie, CookieJar};
async fn handle_request(
_action: &ActionName,
input_data: InputModelName,
_ctx: &HttpContext,
) -> Result<HttpOkResult, HttpFailResult> {
let mut cookies = CookieJar::new();
// Set cookie with options
cookies.set_cookie(
Cookie::new("SessionId", "abc123")
.set_domain("/")
.set_max_age(24 * 60 * 60), // 24 hours
);
// Simple cookie setting
cookies.set_cookie(("Test2".to_string(), "Value".to_string()));
cookies.set_cookie(("Test3", "Value".to_string()));
HttpOutput::from_builder()
.with_cookies(cookies)
.with_content_type(WebContentType::Json)
.with_content(json_bytes)
.build()
.into_ok_result(true).into()
}- Actions receive
Arc<AppContext>for shared application state - The
_appfield is prefixed with_if not directly used in the handler - Swagger documentation is automatically generated from
http_routeannotations - Business logic should be implemented in
scripts/module, not directly in actions - Path parameters in routes must match
#[http_path]fields in input models - Only one body type (
http_body,http_form_data, orhttp_body_raw) can be used per input model - Headers are case-insensitive when reading
- Optional fields use
Option<T>type - Default values can be specified for any input field attribute
- Field transformations (
to_lowercase,to_uppercase,trim) are applied before parsing to_lowercaseandto_uppercaseattributes work only withStringtypes- Enums must have at least one case marked with
defaultif used with default values - Custom input fields must implement
TryIntotrait for conversion from HTTP parameter types - When using
model:in result field, always deriveMyHttpObjectStructure- this is a common mistake that causes compilation errors
- GitHub Wiki - Official documentation and examples
- Keep controllers thin: no inline
map_err. - Convert domain/script errors to
HttpFailResultin one place. - Build HTTP errors via
HttpOutput::from_builder()+HttpFailResult::new. - Controllers only orchestrate and use
?.
src/http_server/errors.rs: all HTTP error helpers andFromconversions.src/http_server/mod.rs: exporterrors.- Controllers import helpers from
crate::http_server::errors.
fn text_error(status: u16, msg: impl Into<String>) -> HttpFailResult;
pub fn bad_request(msg: impl Into<String>) -> HttpFailResult;
pub fn compile_solidity_http(source: &str, contract: Option<&str>)
-> Result<(String, String), HttpFailResult>;
pub fn encode_constructor_args_http(abi: &str, params: &[String])
-> Result<String, HttpFailResult>;
pub async fn deploy_contract_http(
app: &Arc<AppContext>,
private_key: &str,
bytecode: &str,
constructor_args: Option<&str>,
value_eth: Option<f64>,
) -> Result<(TxHash, Address), HttpFailResult>;
pub fn parse_private_key_http(hex: &str)
-> Result<B256, HttpFailResult>;
pub fn signer_from_bytes(key: &B256)
-> Result<PrivateKeySigner, HttpFailResult>;Implement From for domain/script errors in errors.rs, for example:
impl From<SoliditySourceError> for HttpFailResult {
match err {
MissingSource => bad_request("Either solidity_url or solidity_code must be provided"),
InvalidUrl(msg) => bad_request(format!("Invalid solidity_url: {}", msg)),
DownloadFailed { status, message } =>
text_error(status.unwrap_or(400), format!("Failed to download Solidity source: {}", message)),
ReadFailed(msg) => bad_request(format!("Failed to read Solidity source: {}", msg)),
Utf8Error(msg) => HttpFailResult::as_fatal_error(
format!("Failed to decode Solidity source as UTF-8: {}", msg)
),
}
}- Import helpers from
http_server::errors. - Use
?everywhere; avoid inlinemap_err. - Example flow:
let src = fetch_solidity_source(...).await?; // uses From<SoliditySourceError>
let (bytecode, abi) = compile_solidity_http(&src, contract_name)?;
let encoded = encode_constructor_args_http(&abi, ¶ms)?;
let (tx_hash, addr) = deploy_contract_http(app, pk, &bytecode, encoded.as_deref(), value).await?;
let signer = signer_from_bytes(&parse_private_key_http(pk)?)?;
Ok(HttpOutput::as_json(resp).into_ok_result(false)?)bad_request(400): invalid/missing input, download/read failures.text_error(status, msg): propagate upstream status (e.g., download HTTP status).HttpFailResult::as_fatal_error(...): unexpected/internal (compile, encode, deploy, signing, UTF-8 decode).
let out = HttpOutput::from_builder()
.set_status_code(400)
.set_content_type(WebContentType::Text)
.set_content(msg.into_bytes())
.build();
HttpFailResult::new(out, false, false)- Export
errorsinhttp_server/mod.rs. - Centralize conversions in
errors.rs. - Controllers import helpers; avoid inline
map_err. - Business logic stays in
scripts/; controllers orchestrate only. - When using
model:inhttp_route, response structs deriveSerialize,Deserialize,MyHttpObjectStructure.