Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Created February 24, 2026 19:55
Show Gist options
  • Select an option

  • Save masakielastic/478ac9a77f3fb8d3a32ccbbcf480ea7c to your computer and use it in GitHub Desktop.

Select an option

Save masakielastic/478ac9a77f3fb8d3a32ccbbcf480ea7c to your computer and use it in GitHub Desktop.
warp v0.4 で TLS なしの HTTP サーバーその2・JSON レスポンス | PHP 拡張、ext-php-rs

warp v0.4 で TLS なしの HTTP サーバーその2・JSON レスポンス | PHP 拡張、ext-php-rs

コード

Cargo.toml

src/lib.rs

use std::net::SocketAddr;
use std::collections::BTreeMap;
use base64::Engine;
use bytes::Bytes;
use serde::Serialize;

use ext_php_rs::prelude::*;
use hyper::server::conn::http1;
use hyper_util::rt::TokioIo;
use hyper_util::service::TowerToHyperService;
use tokio::net::TcpListener;
use warp::Filter;
use warp::http::{HeaderMap, Method, StatusCode};
use warp::path::FullPath;

fn php_err<E: std::fmt::Display>(e: E) -> PhpException {
    PhpException::default(e.to_string())
}

#[derive(Serialize)]
struct DebugRequest {
    method: String,
    path: String,
    query: String,
    headers: BTreeMap<String, String>,
    body: DebugBody,
}

#[derive(Serialize)]
#[serde(tag = "type", content = "value")]
enum DebugBody {
    Empty,
    Utf8(String),
    Base64(String),
}

fn headers_to_map(headers: HeaderMap) -> BTreeMap<String, String> {
    let mut map = BTreeMap::new();
    for (name, value) in headers.iter() {
        let key = name.to_string();
        let val = match value.to_str() {
            Ok(s) => s.to_string(),
            Err(_) => {
                // 非UTF-8は bytes を base64 表現で入れる(ヘッダは基本UTF-8想定だが安全側)
                let b64 = base64::engine::general_purpose::STANDARD.encode(value.as_bytes());
                format!("base64:{b64}")
            }
        };
        map.insert(key, val);
    }
    map
}

fn bytes_to_body(body: Bytes) -> DebugBody {
    if body.is_empty() {
        return DebugBody::Empty;
    }
    match std::str::from_utf8(body.as_ref()) {
        Ok(s) => DebugBody::Utf8(s.to_string()),
        Err(_) => {
            let b64 = base64::engine::general_purpose::STANDARD.encode(body.as_ref());
            DebugBody::Base64(b64)
        }
    }
}

async fn serve_once(
    host: String,
    port: u16,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    // /debug に来たリクエストを“解析してそのまま返す”


let debug = warp::path("debug")
    .and(warp::path::end())
    .and(warp::method())
    .and(warp::path::full())
    .and(
        warp::query::raw()
            .or_else(|_| async { Ok::<(String,), std::convert::Infallible>((String::new(),)) }),
    )
    .and(warp::header::headers_cloned())
    .and(warp::body::content_length_limit(1024 * 1024).and(warp::body::bytes()))
    .and_then(
        |method: Method,
         path: FullPath,
         query: String,
         headers: HeaderMap,
         body: Bytes| async move {
            let payload = DebugRequest {
                method: method.to_string(),
                path: path.as_str().to_string(),
                query,
                headers: headers_to_map(headers),
                body: bytes_to_body(body),
            };

            let json = serde_json::to_string_pretty(&payload).unwrap_or_else(|e| {
                format!(r#"{{"error":"serde_json failed","detail":"{}"}}"#, e)
            });

            Ok::<_, std::convert::Infallible>(warp::reply::with_status(
                warp::reply::with_header(json, "content-type", "application/json; charset=utf-8"),
                StatusCode::OK,
            ))
        },
    );

    // それ以外は通常応答
    let root = warp::path::end().map(|| "Hello, World!".to_string());

    let routes = debug.or(root);

    let tower_svc = warp::service(routes);
    let hyper_svc = TowerToHyperService::new(tower_svc);

    let addr: SocketAddr = format!("{host}:{port}").parse()?;
    let listener = TcpListener::bind(addr).await?;
    eprintln!("listening on http://{addr}");

    loop {
        let (stream, _peer) = listener.accept().await?;
        let io = TokioIo::new(stream);
        let svc = hyper_svc.clone();

        tokio::spawn(async move {
            if let Err(err) = http1::Builder::new().serve_connection(io, svc).await {
                eprintln!("error serving connection: {err}");
            }
        });
    }
}

#[php_function]
pub fn warp_http_serve(host: String, port: i64) -> PhpResult<()> {
    if port < 0 || port > 65535 {
        return Err(php_err("port must be in 0..65535"));
    }
    let port = port as u16;

    // PHP 呼び出しをブロックして Rust 側のサーバを回す(最小例)
    let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .map_err(php_err)?;

    rt.block_on(async move { serve_once(host, port).await })
        .map_err(php_err)?;

    Ok(())
}


#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
    module.function(wrap_function!(warp_http_serve))
}

ビルドと実行

cargo build
php -d extension=target/debug/libphp_warp_server.so -r 'warp_http_serve("127.0.0.1", 3000);'
curl -s "http://127.0.0.1:3000/debug?x=1&y=2"   -H "X-Test: 123"   -d "name=masaki" | jq .
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment