apollo-http-client 0.3.0

HTTP client for Apollo platform
Documentation
//! Integration tests for ALPN protocol negotiation.
#![allow(unused_imports)]

#[macro_use]
mod common;

use std::net::SocketAddr;
use std::sync::Arc;

use apollo_http_client::{HttpBody, HttpClient, HttpClientConfig};
use apollo_opentelemetry::metrics::Clock;
use apollo_opentelemetry_test::{TelemetryContext, assert_metrics_snapshot};
use axum::Router;
use axum::routing::get;
use bytes::Bytes;
use http_body_util::{BodyExt as _, Full};
use hyper_util::rt::{TokioExecutor, TokioIo};
use indoc::indoc;
use opentelemetry_sdk::metrics::{Aggregation, Instrument, StreamBuilder};
use tower::ServiceExt as _;

use common::*;

// --- Auto: infrastructure -------------------------------------------------

fn auto_config() -> HttpClientConfig {
    parse_config("protocol: alpn\ntls:\n  danger_accept_invalid_certs: true")
}

// --- Auto: error path -----------------------------------------------------

/// When `Protocol::Alpn` cannot connect, `error.type: "_OTHER"` is recorded
/// with `network.protocol.version: "1.1"` — the initial placeholder assigned
/// before ALPN negotiation. The guard `.set()` call that would update the
/// version to `"2"` is only reached on a successful response, so the error
/// path always records the initial version.
#[tokio::test]
async fn auto_connection_refused_records_error() {
    let ctx = TelemetryContext::new();

    let config = parse_config("protocol: alpn\nconnect_timeout: 1s");
    let req = http::Request::builder()
        .method(http::Method::GET)
        .uri("http://127.0.0.1:1/")
        .body(empty_body())
        .unwrap();

    let err = new_client(&config)
        .oneshot(req)
        .await
        .expect_err("should fail");
    assert!(matches!(
        err,
        apollo_http_client::HttpClientError::Transport { .. }
    ));

    assert_metrics_snapshot!(ctx, @r#"
    - name: http.client.active_requests
      description: Number of HTTP requests currently in flight
      unit: "{request}"
      data:
        type: Sum
        data_points:
          - attributes:
              http.request.method: GET
              server.address: 127.0.0.1
              server.port: "1"
            value: 0
        is_monotonic: false
        temporality: Cumulative
    - name: http.client.open_connections
      description: Number of open connections in the HTTP client pool
      unit: "{connection}"
      data:
        type: Sum
        data_points:
          - attributes:
              http.connection.state: connecting
              server.address: 127.0.0.1
              server.port: "1"
            value: 0
        is_monotonic: false
        temporality: Cumulative
    - name: http.client.request.duration
      description: Duration of HTTP client requests
      unit: s
      data:
        type: Histogram
        data_points:
          - attributes:
              error.type: _OTHER
              http.request.method: GET
              network.protocol.version: "1.1"
              server.address: 127.0.0.1
              server.port: "1"
            count: 1
            sum: 0
            min: 0
            max: 0
            bounds:
              - 0.005
              - 0.01
              - 0.025
              - 0.05
              - 0.075
              - 0.1
              - 0.25
              - 0.5
              - 0.75
              - 1
              - 2.5
              - 5
              - 7.5
              - 10
            bucket_counts:
              - 1
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
        temporality: Cumulative
    "#);
}

// --- Auto: success path ---------------------------------------------------

/// With `Protocol::Alpn` over plain HTTP the connection falls back to HTTP/1.1.
/// All metrics record `network.protocol.version: "1.1"`.
#[tokio::test]
async fn auto_plain_http_uses_h1() {
    let ctx = integration_context();

    let addr = spawn_server(Router::new().route("/", get(|| async { "hello" }))).await;
    let req = http::Request::builder()
        .method(http::Method::GET)
        .uri(format!("http://{addr}/"))
        .body(empty_body())
        .unwrap();

    let resp = new_client(&parse_config("protocol: alpn"))
        .oneshot(req)
        .await
        .expect("request ok");
    resp.into_body().collect().await.unwrap();

    snapshot!(ctx, @r#"
    - name: http.client.active_requests
      description: Number of HTTP requests currently in flight
      unit: "{request}"
      data:
        type: Sum
        data_points:
          - attributes:
              http.request.method: GET
              server.address: 127.0.0.1
              server.port: "<port>"
            value: 0
        is_monotonic: false
        temporality: Cumulative
    - name: http.client.open_connections
      description: Number of open connections in the HTTP client pool
      unit: "{connection}"
      data:
        type: Sum
        data_points:
          - attributes:
              http.connection.state: active
              network.protocol.version: "1.1"
              server.address: 127.0.0.1
              server.port: "<port>"
            value: 0
          - attributes:
              http.connection.state: connecting
              server.address: 127.0.0.1
              server.port: "<port>"
            value: 0
          - attributes:
              http.connection.state: idle
              network.protocol.version: "1.1"
              server.address: 127.0.0.1
              server.port: "<port>"
            value: 0
        is_monotonic: false
        temporality: Cumulative
    - name: http.client.request.duration
      description: Duration of HTTP client requests
      unit: s
      data:
        type: Histogram
        data_points:
          - attributes:
              http.request.method: GET
              http.response.status_code: "200"
              network.protocol.version: "1.1"
              server.address: 127.0.0.1
              server.port: "<port>"
            count: 1
            sum: 0
            min: 0
            max: 0
            bounds:
              - 0.005
              - 0.01
              - 0.025
              - 0.05
              - 0.075
              - 0.1
              - 0.25
              - 0.5
              - 0.75
              - 1
              - 2.5
              - 5
              - 7.5
              - 10
            bucket_counts:
              - 1
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
        temporality: Cumulative
    "#);
}

/// With `Protocol::Alpn` over TLS, ALPN negotiates HTTP/2.
/// Connection-level metrics (`open_connections`) record `version: "2"` from
/// the start. Request-level metrics are initially registered as "1.1" (before
/// the protocol is known) and updated to "2" via `TrackGuard::set` /
/// `RecordDurationGuard::set` once the response arrives — except
/// `request.body.size`, which is moved into the body stream before the
/// protocol is known.
#[tokio::test]
async fn auto_negotiates_h2_over_tls() {
    let ctx = integration_context();

    let addr = spawn_tls_h2_server(Router::new().route("/", get(|| async { "hello" }))).await;
    let req = http::Request::builder()
        .method(http::Method::GET)
        .uri(format!("https://{addr}/"))
        .body(empty_body())
        .unwrap();

    let resp = new_client(&auto_config())
        .oneshot(req)
        .await
        .expect("request ok");
    resp.into_body().collect().await.unwrap();

    snapshot!(ctx, @r#"
    - name: http.client.active_requests
      description: Number of HTTP requests currently in flight
      unit: "{request}"
      data:
        type: Sum
        data_points:
          - attributes:
              http.request.method: GET
              server.address: 127.0.0.1
              server.port: "<port>"
            value: 0
        is_monotonic: false
        temporality: Cumulative
    - name: http.client.open_connections
      description: Number of open connections in the HTTP client pool
      unit: "{connection}"
      data:
        type: Sum
        data_points:
          - attributes:
              http.connection.state: active
              network.protocol.version: "2"
              server.address: 127.0.0.1
              server.port: "<port>"
            value: 0
          - attributes:
              http.connection.state: connecting
              server.address: 127.0.0.1
              server.port: "<port>"
            value: 0
          - attributes:
              http.connection.state: idle
              network.protocol.version: "2"
              server.address: 127.0.0.1
              server.port: "<port>"
            value: 0
        is_monotonic: false
        temporality: Cumulative
    - name: http.client.request.duration
      description: Duration of HTTP client requests
      unit: s
      data:
        type: Histogram
        data_points:
          - attributes:
              http.request.method: GET
              http.response.status_code: "200"
              network.protocol.version: "2"
              server.address: 127.0.0.1
              server.port: "<port>"
            count: 1
            sum: 0
            min: 0
            max: 0
            bounds:
              - 0.005
              - 0.01
              - 0.025
              - 0.05
              - 0.075
              - 0.1
              - 0.25
              - 0.5
              - 0.75
              - 1
              - 2.5
              - 5
              - 7.5
              - 10
            bucket_counts:
              - 1
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
              - 0
        temporality: Cumulative
    "#);
}