dropshot

repository·main·Indexed 22 days ago

https://github.com/oxidecomputer/dropshot

An opinionated, lightweight Rust crate for exposing REST APIs. It provides first-class support for OpenAPI specification generation and consistent pagination. The library allows developers to define API traits using the #[dropshot::api_description] macro and expose methods as endpoints via the #[endpoint] attribute.

Tokens
26.5K
Snippets
63
Records
96
Agent score
78%

What's inside dropshot

  1. Overview of Dropshot

    main
    Dropshot is an opinionated, lightweight Rust crate designed for exposing REST APIs. It focuses on simplicity and provides first-class support for OpenAPI by generating precise specifications directly from your code. Additionally, it includes built-in primitives for consistent pagination, including OpenAPI extensions to denote pagination in the generated spec.
  2. Migrate RequestContext and Extractors in v0.9.0

    main

    Version 0.9.0 introduced significant changes to how RequestContext and Extractors are handled to improve type safety and performance.

    1. Remove Arc from RequestContext

    RequestContext is no longer wrapped in an Arc. Endpoint functions and extractors now accept RequestContext<T> directly. Action: Change the first argument of every endpoint function from Arc<RequestContext<T>> to RequestContext<T>.

    2. Split Extractors into Shared and Exclusive

    The Extractor trait was split into SharedExtractor and ExclusiveExtractor to prevent runtime errors when multiple extractors attempt to access the request body.

    • SharedExtractor: For extractors that only need to read headers, the URL, or the method (does not consume the body).
    • ExclusiveExtractor: For extractors that need to read the request body (e.g., TypedBody, UntypedBody, WebsocketConnection, or RawRequest).

    Rules for Exclusive Extractors:

    • An endpoint can have at most one ExclusiveExtractor.
    • The ExclusiveExtractor must be the last argument in the endpoint function signature.

    3. Accessing the Raw Request

    To access the raw hyper::Request, use the RawRequest extractor instead of accessing rqctx.request (which was previously behind a Mutex). Action: Add raw_request: RawRequest as the last argument to your endpoint, then use let request = raw_request.into_inner();.

  3. How Dropshot handles request middleware and shared logic

    main

    Unlike many web frameworks that use a chain of middleware handlers (which can make control flow and implicit dependencies difficult to track), Dropshot encourages using explicit function calls to share logic between handlers.

    Instead of a middleware that modifies a request object (e.g., adding an auth field), Dropshot's design pattern involves utility functions that return typed context objects.

    Example Pattern:

    1. An authentication utility function is called within a handler.
    2. This function returns an AuthzContext struct.
    3. Any subsequent logic requiring authorization consumes that AuthzContext as a direct function argument.

    This approach ensures that dependencies are explicit: if a handler requires an AuthzContext, it must call the function to obtain it, making the code easier to reason about and maintain.

  4. Implement custom error types with HttpResponseError

    main
    Starting in v0.15.0, endpoint handlers are no longer restricted to returning dropshot::HttpError. They can return any error type that implements the dropshot::HttpResponseError trait. This allows you to use domain-specific error types and automatically generate OpenAPI response schemas for them.
  5. Upgrade to hyper 1.0 and http 1.0 in v0.12.0

    main

    Dropshot v0.12.0 updated to support hyper 1.0 and http 1.0. When upgrading, you must also update your own crate's dependencies to these versions.

    Steps to upgrade:

    1. Update hyper and http to 1.0 (or newer compatible versions) in your Cargo.toml.
    2. Replace all references to hyper::Body with dropshot::Body.
    3. You may need to use http-body-util to assist with dropshot::Body manipulations.
  6. Migrate to ErrorStatusCode in v0.15.0

    main

    In version 0.15.0, HttpError was changed to use dropshot::ErrorStatusCode instead of http::StatusCode. ErrorStatusCode is a newtype wrapper around http::StatusCode that is restricted to 4xx and 5xx status codes.

    To update your code:

    1. Replace http::StatusCode::... constants with dropshot::ErrorStatusCode::....
    2. For non-standard status codes, use ErrorStatusCode::from_u16(code).
    3. Note that ErrorStatusCode implements TryFrom<http::StatusCode> for easy conversion from external sources.
    // Before
    dropshot::HttpError {
        status: http::StatusCode::NOT_FOUND,
        // ...
    }
    
    // After
    dropshot::HttpError {
        status: dropshot::ErrorStatusCode::NOT_FOUND,
        // ...
    }
    
    // For extension codes
    dropshot::ErrorStatusCode::from_u16(420).expect("420 is a valid 4xx status code")
  7. Migrate to the new HttpServer API

    main

    The HttpServer implementation has been split into two distinct types to better separate server configuration from a running server instance:

    1. HttpServerStarter: Used to construct and configure the server. Use HttpServerStarter::start() to begin execution.
    2. HttpServer: Represents the active, running server.

    Key changes in behavior:

    • Starting the server: Instead of calling .run() on an HttpServer object, you now call .start() on an HttpServerStarter object, which returns an HttpServer.
    • Awaiting shutdown: In previous versions, HttpServer returned a tokio::JoinHandle and required wait_for_shutdown. In the new version, HttpServer implements Future and can be directly .await-ed to wait for the server to complete.
    // Old Version:
    let mut server = HttpServer::new( /* Arguments are the same between versions */ )
      .map_err(|error| format!("failed to start server: {}", error))?;
    
    let server_task = server.run();
    server.wait_for_shutdown(server_task).await;
    
    // New Version
    let server = HttpServerStarter::new( /* Arguments are the same between versions */ )
      .map_err(|error| format!("failed to start server: {}", error))?
      .start();
    
    server.await;
  8. Migrate to ServerBuilder in v0.13.0

    main

    The ServerBuilder is now the primary way to construct a Dropshot server, replacing HttpServerStarter. The builder provides structured errors and is the only way to add new construction-time options (like API versioning) in the future.

    Migration Patterns:

    Standard (Non-TLS):

    // Old
    HttpServerStarter::new(&config, api, private, &log).map_err(...)? .start()
    
    // New
    ServerBuilder::new(api, private, log).config(config).start().map_err(...)?

    With TLS:

    // Old
    HttpServerStarter::new_with_tls(&config, api, private, &log, tls).map_err(...)? .start()
    
    // New
    ServerBuilder::new(api, private, log).config(config).tls(tls).start().map_err(...)?

    Intermediate Starter: If you need to hold a starter object before calling .start(), use .build_starter():

    let starter = ServerBuilder::new(api, private, log).config(config).build_starter().map_err(...)?;
    // ...
    starter.start();
  9. Publish a new release

    main

    Dropshot consists of two packages, dropshot and dropshot_endpoint, which are always released together with the same version number. A release involves creating a git tag with the version number and publishing both packages to crates.io.

    Release Workflow

    1. Prepare Changelog: Ensure CHANGELOG.adoc is up to date. Breaking changes should include concise summaries of how users are affected and how to upgrade safely.
    2. Verify Working Directory:
      • Ensure you are on the main branch and synced with upstream (git pull).
      • Ensure git status shows no uncommitted files.
      • Run git clean -nxd to ensure no stale build artifacts are present.
      • Verify that CI has passed for your current commit on GitHub.
    3. Identify Release Metadata:
      • Find the PREV_RELEASE_TAG (the tag used for the most recent release).
      • Determine the RELEASE_KIND (e.g., patch, minor, or major) or specify a NEW_VERSION.
    4. Execute Release:
      • Install/update cargo-release.
      • Run the cargo release command.
    5. Finalize:
      • Push the resulting commit and tags to the remote repository.

    Note: You may need to temporarily allow administrators to override branch protection to push the release commit and tags.

    # Install cargo-release
    cargo install cargo-release
    
    # Execute the release
    $ cargo release --prev-tag-name=PREV_RELEASE_TAG -vv --execute RELEASE_KIND|NEW_VERSION
    
    # Push changes
    $ git push
    $ git push --tags
  10. Configure tag policies with `tag_config` in `#[dropshot::api_description]`

    main

    You can organize your OpenAPI documentation using tags. By providing a tag_config argument to the #[dropshot::api_description] macro, you can enforce compliance at runtime during endpoint registration.

    Supported fields in tag_config:

    • tags: A map of tag names to their metadata (description, external documentation).
    • allow_other_tags: Boolean. If false, any tag not explicitly defined in the tags map will cause a registration error. Defaults to false if tag_config is provided.
    • policy: An EndpointTagPolicy (e.g., Any, ExactlyOne) that defines how many tags an endpoint must have.