Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Created February 23, 2026 10:18
Show Gist options
  • Select an option

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

Select an option

Save masakielastic/fe8c6b3264ee5be0145d8803937a1486 to your computer and use it in GitHub Desktop.
tiny_http サーバーでシングルスレッドの HTTP/1 サーバー| PHP 拡張 | Rust

tiny_http サーバーでシングルスレッドの HTTP/1 サーバー| PHP 拡張 | Rust

Cargo.toml

[package]
name = "php_httpd_ext"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
ext-php-rs = "0.15"
tiny_http = "0.12"

src/lib.rs

use ext_php_rs::prelude::*;
use tiny_http::{Header, Response, Server, StatusCode};
use std::io::Read;

fn header(name: &str, value: &str) -> Header {
    Header::from_bytes(name.as_bytes(), value.as_bytes()).expect("invalid header")
}

#[php_function]
pub fn httpd_run(
    addr: String,
    body: String,
    max_requests: i64, // 0 or negative => infinite
    content_type: Option<String>,
) -> PhpResult<String> {
    let server = Server::http(&addr)
        .map_err(|e| PhpException::default(format!("failed to bind {}: {}", addr, e)))?;

    let mut handled: i64 = 0;
    let ct = content_type.unwrap_or_else(|| "text/plain; charset=utf-8".to_string());

    for request in server.incoming_requests() {
        // 固定レスポンス(逐次処理)
        let response = Response::from_string(body.clone())
            .with_status_code(StatusCode(200))
            .with_header(header("Content-Type", &ct))
            .with_header(header("Connection", "close"));

        // 送信(エラーはログ文字列として返す方針もあるが、ここでは握りつぶさず例外化)
        if let Err(e) = request.respond(response) {
            return Err(PhpException::default(format!("respond failed: {}", e)));
        }

        handled += 1;

        if max_requests > 0 && handled >= max_requests {
            break;
        }
    }

    Ok(format!(
        "httpd_run finished: addr={}, handled={}",
        addr, handled
    ))
}

#[php_function]
pub fn httpd_run_echo(addr: String, max_requests: i64) -> PhpResult<String> {
    let server = Server::http(&addr)
        .map_err(|e| PhpException::default(format!("failed to bind {}: {}", addr, e)))?;

    let mut handled: i64 = 0;

    for mut request in server.incoming_requests() {
        let url = request.url().to_string();
        let method = request.method().as_str().to_string();

        let mut out = String::new();
        out.push_str("tiny_http + ext-php-rs echo server\n\n");
        out.push_str(&format!("method: {}\n", method));
        out.push_str(&format!("url: {}\n", url));
        out.push_str("\nheaders:\n");
        for h in request.headers() {
            out.push_str(&format!("  {}: {}\n", h.field.as_str(), h.value.as_str()));
        }

        // body も読む(大きいボディは注意。ここは最大 64KB まで)
        out.push_str("\nbody (up to 65536 bytes):\n");
        let mut buf = Vec::new();
        let _ = request
            .as_reader()
            .take(65536)
            .read_to_end(&mut buf);
        out.push_str(&String::from_utf8_lossy(&buf));

        let response = Response::from_string(out)
            .with_status_code(StatusCode(200))
            .with_header(header("Content-Type", "text/plain; charset=utf-8"))
            .with_header(header("Connection", "close"));

        if let Err(e) = request.respond(response) {
            return Err(PhpException::default(format!("respond failed: {}", e)));
        }

        handled += 1;
        if max_requests > 0 && handled >= max_requests {
            break;
        }
    }

    Ok(format!(
        "httpd_run_echo finished: addr={}, handled={}",
        addr, handled
    ))
}

#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
    module
        .name("httpd_ext")
        .function(wrap_function!(httpd_run))
        .function(wrap_function!(httpd_run_echo))
}
php -d extension=target/debug/libphp_httpd_ext.so -r '
echo httpd_run("127.0.0.1:8080", "Hello from PHP extension\n", 3, "text/plain; charset=utf-8"), PHP_EOL;
'
httpd_run finished: addr=127.0.0.1:8080, handled=3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment