Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add FastCgiHttpMessageHandler with basic tests #2572

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

funcmike
Copy link

This allows to send normal HTTP requests using FastCGI protocol.
Implementation is based on Caddy (https://github.com/caddyserver/caddy/tree/master/modules/caddyhttp/reverseproxy/fastcgi). It's a working draft - basic test performed with PHP-FPM & Go.
I need feedback / review - I'm open to discuss how to progress with it and make it mergeable.

Solves: #85

@Tratcher
Copy link
Member

Interesting the whole thing is just a HttpMessageHandler. Does it have uses outside of yarp that would warrant it being its own project?

@funcmike
Copy link
Author

I tried to make it as HttpMessageHandler to see how far I can go with it. It turn out doable with little tricks like DOCUMENT_ROOT is passed as HttpOption in request - its static option so it could be also passed in constructor.
I don’t know if it’s useful for others, nowadays FastCGI is used mainly by PHP but let others give opinion on that.

@funcmike
Copy link
Author

@Tratcher Tests are still just for me to debug responses (I will add more & better at some point), but code of FastCgiHttpMessageHandler can be reviewed. I need some feedback to known if I'm going in a right direction with it.

src/ReverseProxy/Forwarder/FastCgiHttpMessageHandler.cs Outdated Show resolved Hide resolved
src/ReverseProxy/Forwarder/FastCgiHttpMessageHandler.cs Outdated Show resolved Hide resolved
src/ReverseProxy/Forwarder/FastCgiHttpMessageHandler.cs Outdated Show resolved Hide resolved
src/ReverseProxy/Forwarder/FastCgiHttpMessageHandler.cs Outdated Show resolved Hide resolved
src/ReverseProxy/Forwarder/FastCgiHttpMessageHandler.cs Outdated Show resolved Hide resolved
src/ReverseProxy/Forwarder/FastCgiHttpMessageHandler.cs Outdated Show resolved Hide resolved
fastCgiRecord.Dispose();
return true;
}
case FastCgiRecordHeader.RecordType.EndRequest:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are trailers supported by the protocol?

Copy link
Author

@funcmike funcmike Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whole HTTP response in FastCgi Response is just "ContentData" in STDOUT record.
There is no special support for Header, Body, or Trailers. FastCgiParams are only for Request. Response is just bytes that need to be parsed. So Trailers are "supported". I need to add tests for it but I think in current implementation I'm sending everything after headers so trailers too, but maybe I need to do something special in HttpResponseMessage?

}
finally
{
input.AdvanceTo(buffer.Start, buffer.End);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AdvanceTo can happen first, without a try/finally, because you're not using the content.

Comment on lines +213 to +219
if (prevBuffLenOnCompleted is { } prevLen && prevLen == buffer.Length)
{
input.AdvanceTo(buffer.Start, buffer.End);
throw new BadFastCgiResponseException(FastCgiCoreExpStrings.BadResponse_IncompleteRecord, FastCgiResponseRejectionReason.IncompleteRecord);
}

prevBuffLenOnCompleted = buffer.Length;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to check for this around line 228 and 230. If it can't read the record, and there's still data left, but the pipe is completed, then error.

Comment on lines +267 to +284
if (header.ContentLength == 0)
{
fastCgiRecord = new FastCgiRecord(
Header: header,
ContentData: ReadOnlySequence<byte>.Empty);

// Advance the buffer
buffer = buffer.Slice(recordByteCount);
return true;
}

fastCgiRecord = new FastCgiRecord(
Header: header,
ContentData: buffer.Slice(FastCgiRecordHeader.FCGI_HEADER_LEN, header.ContentLength));

// Advance the buffer
buffer = buffer.Slice(recordByteCount);
return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove duplication

Suggested change
if (header.ContentLength == 0)
{
fastCgiRecord = new FastCgiRecord(
Header: header,
ContentData: ReadOnlySequence<byte>.Empty);
// Advance the buffer
buffer = buffer.Slice(recordByteCount);
return true;
}
fastCgiRecord = new FastCgiRecord(
Header: header,
ContentData: buffer.Slice(FastCgiRecordHeader.FCGI_HEADER_LEN, header.ContentLength));
// Advance the buffer
buffer = buffer.Slice(recordByteCount);
return true;
fastCgiRecord = new FastCgiRecord(
Header: header,
ContentData: header.ContentLength == 0 ?
ReadOnlySequence<byte>.Empty :
buffer.Slice(FastCgiRecordHeader.FCGI_HEADER_LEN, header.ContentLength));
// Advance the buffer
buffer = buffer.Slice(recordByteCount);
return true;

_interruptOnState = interruptOnState;
}

public ValueTask<FlushResult> FlushAsync(CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does a reader have a flush method?

Copy link
Author

@funcmike funcmike Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because current stream design is like that

First IO Pipe FastCgi Server Connection Reader -> Process FastCGI Record (only 1 STDOUT Record and interrupt) -> Unpack content -> Second IO Pipe Internal Writer Stream -> Flush (make it available for reader) -> Second IO Pipe Reader internal Stream ... so reading stream is causing blocking and waiting for the 1 next frame / record to be buffered in IO Pipe Writer.

Internal Stream Pipe is in FastCgiMessageHandler so that's why it has Flush method.

So its kinda a Extract Transform Load when Reader becomes also feeder/writer to second pipe that is consumed by final client.

{
if (!_readerFinished)
{
if (await FastCgiRecordReader.ProcessAsync(connection, handler, cancellationToken))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProcessAsync has a while loop in it that could cause it to read to the end of the response, no?

This Stream.ReadAsync API should return as soon as the given buffer is full. I'd expect a sequence more like

  • do I have part of a prior message left? Copy that to the buffer
  • while I have more space in the buffer, try reading the next message and copying that to the buffer
  • store any partial message for future reads

Copy link
Author

@funcmike funcmike Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, while loop is always blocking stream( after http Headers) only for just 1 Record/Frame. So it's works similar to what You described, responses (set of records) are partially (frame by frame) buffered in IO PipeWriter and waits for next stream read.
So every 1 Stream read call is causing a buffer filling for only 1 or 0 (finish) Records.
1 frame/record is max 65k bytes - in-practice php-fpm instances are very close to Proxy/Loadbalancer (same machines/node - internal connection) so 1 frame will be transferred very fast and my assumptions is that storing of partial / incomplete frame data is not needed - so I'm waiting until connection IO Reader has full frame.

Comment on lines +872 to +873
public override bool CanSeek => stream.CanSeek;
public override bool CanWrite => stream.CanWrite;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

false

public override bool CanRead => stream.CanRead && !_disposed;
public override bool CanSeek => stream.CanSeek;
public override bool CanWrite => stream.CanWrite;
public override long Length => stream.Length;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotSupportedException

Comment on lines +920 to +922
public override long Length { get; } = 0;

public override long Position { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotSupportedException

public override void SetLength(long value) => throw new NotSupportedException();
}

private static class HttpResponseHeaders
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpResponseHeaders already exists

Suggested change
private static class HttpResponseHeaders
private static class FastCgiResponseHeaders

void WriteTo(IBufferWriter<byte> buffer);
}

private static class FastCgiRecordReader
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please split this file up across multiple files by type, reviewing one giant file makes it hard to find things.

@povilas-st
Copy link

🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants