RandomInsano
RandomInsano

Reputation: 1260

How to close an HTTP TCP stream on the server after a request

I'm creating an Open Metrics HTTP source for a Prometheus server in Rust. I'd rather avoid pulling in a whole web server library (crate) because I control the infrastructure and pretending to answer HTTP/1.1 is easy enough.

What's a bit surprising is that I'm getting failures from various clients:

I've tried both adding the "Connection" and "Content-Length" headers to try and get around this. Right now this is what's coming out of netcat:

$ nc localhost 3003
HTTP/1.1 200 OK
Content-Type: text/plain; version=0.0.4; charset=utf-8; escaping=underscores
Connection: close
Content-Length: 111

# HELP requests Number of requests received.
# TYPE requests counter
requests_total{method="Get"} 8
# EOF

I'm letting the socket go out of scope. This is where the Rust stuff starts:

let method = Method::Get;
for stream in listener.incoming() {
    let mut stream = stream.unwrap();

    metrics.inc_requests(method.clone());

    println!("Connection established!");

    let mut response_buffer = HTTP_HEADERS.to_string();
    response_buffer.push_str(&format!("Content-Length: {}", response_buffer.len()));
    response_buffer.push_str("\n\n");

    encode(&mut response_buffer, &registry).unwrap();

    stream.write(response_buffer.as_bytes()).unwrap();
}

Upvotes: 0

Views: 58

Answers (1)

RandomInsano
RandomInsano

Reputation: 1260

Ah, it turns out there's a shutdown function that can be used. I was relying on the socket going out of scope. This also removes the content-length requirement. I'm sure this isn't Rust-specific. Skip to the last update for a solution.

let method = Method::Get;
for stream in listener.incoming() {
    let mut stream = stream.unwrap();

    metrics.inc_requests(method.clone());

    println!("Connection established!");

    let mut response_buffer = HTTP_HEADERS.to_string();

    encode(&mut response_buffer, &registry).unwrap();

    stream.write(response_buffer.as_bytes()).unwrap();
    stream.shutdown(std::net::Shutdown::Both).unwrap();
}

This doesn't resolve the problem 100% in Safari (some requests succeed, some don't). But really it just needs to work with Prometheus for now.


Update 1: Reading from the socket first didn't make Safari happy (note the code has been refactored a bit)

fn send_response(mut stream: TcpStream, registry: &Arc<Registry>) -> Result<(), MetricsError> {
    let mut response_buffer = HTTP_HEADERS.to_string();
    let mut request_buffer = Vec::new();

    encode(&mut response_buffer, registry)?;

    stream.read(&mut request_buffer)?;

    stream.write(response_buffer.as_bytes())?;
    stream.flush()?;
    stream.shutdown(Shutdown::Both)?;

    Ok(())
}

Update 2: Reliably solved! The problem was specifically not reading a sized buffer. Thanks for everyone's comments:

fn send_response(mut stream: TcpStream, registry: &Arc<Registry>) -> Result<(), MetricsError> {
    let mut response_buffer = HTTP_HEADERS.to_string();
    let mut request_buffer = [0u8; HTTP_REQUEST_BUFFER_SIZE];

    encode(&mut response_buffer, registry)?;

    stream.read(&mut request_buffer)?;
    stream.write_all(response_buffer.as_bytes())?;
    stream.flush()?;
    stream.shutdown(Shutdown::Both)?;

    Ok(())
}

Upvotes: 0

Related Questions