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

Relayer transfroms #612

Merged
merged 12 commits into from
Dec 28, 2020
Merged

Relayer transfroms #612

merged 12 commits into from
Dec 28, 2020

Conversation

Tratcher
Copy link
Member

@Tratcher Tratcher commented Dec 11, 2020

#594 IHttpProxy is getting much more attention/usage than other parts of YARP. Long term we're interested in moving that component into ASP.NET Core. However, IHttpProxy also brings in the entire transforms infrastructure which it's not clear if people using IHttpProxy directly need, or if they could use a simplified model (e.g. callbacks).

Here's the proposed new transforms model for IHttpProxy:

    public record HttpTransforms
    {
        public static readonly HttpTransforms Empty = new HttpTransforms();

        /// <summary>
        /// Indicates if all request headers should be copied to the outgoing HttpRequestMessage before invoking
        /// OnRequest. Disable this if you want to manually control which headers are copied.
        /// Note this copy excludes certain protocol headers like HTTP/2 psuedo headers (":authority").
        /// </summary>
        public bool CopyRequestHeaders { get; init; } = true;

        /// <summary>
        /// Indicates if all response headers should be copied from the received HttpResponseMessage before invoking
        /// OnResponse. Disable this if you want to manually control which headers are copied.
        /// Note this copy excludes certain protocol headers like `Transfer-Encoding: chunked`.
        /// </summary>
        public bool CopyResponseHeaders { get; init; } = true;

        /// <summary>
        /// Indicates if all response trailers should be copied from the received HttpResponseMessage before invoking
        /// OnResponseTrailers. Disable this if you want to manually control which headers are copied.
        /// </summary>
        public bool CopyResponseTrailers { get; init; } = true;

        /// <summary>
        /// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage fields are
        /// initialized except RequestUri, which will be initialized after the callback if no value is provided.
        /// The string parameter represents the destination URI prefix that should be used when constructing the RequestUri.
        /// The headers will be copied before the callback if CopyRequestHeaders is enabled.
        /// </summary>
        public Func<HttpContext, HttpRequestMessage, string, Task> OnRequest { get; init; }

        /// <summary>
        /// A callback that is invoked when the proxied response is received. The status code and reason phrase will be copied
        /// to the HttpContext.Response before the callback is invoked, but may still be modified there. The headers will be
        /// copied to HttpContext.Response.Headers before the callback if CopyResponseHeaders is enabled.
        /// </summary>
        public Func<HttpContext, HttpResponseMessage, Task> OnResponse { get; init; }

        /// <summary>
        /// A callback that is invoked after the response body to modify trailers, if supported. The trailers will be
        /// copied to the HttpContext.Response before the callback if CopyResponseTrailers is enabled.
        /// </summary>
        public Func<HttpContext, HttpResponseMessage, Task> OnResponseTrailers { get; init; }
    }

The idea is that everything should just work but you can get in and tweak things as needed. The higher layer uses these callbacks to hook in and run about the same structured transforms logic it was doing before. There's a little code duplication around header copying to to investigate, and some tests that need cleaning up.

@Tratcher Tratcher self-assigned this Dec 11, 2020
@Tratcher Tratcher force-pushed the tratcher/transforms-split branch 3 times, most recently from c9e5d98 to 657488c Compare December 14, 2020 23:33
@davidfowl
Copy link
Contributor

davidfowl commented Dec 15, 2020

The design change looks good. About the API, I think the type should be abstract and the methods should be virtual instead of delegates. Then maybe the base class can do the copying instead of exposing those properties.

public abstract class HttpTransforms
{
    public static readonly HttpTransforms Default = new DefaultHttpTransforms();

    /// <summary>
    /// A callback that is invoked prior to sending the proxied request. All HttpRequestMessage fields are
    /// initialized except RequestUri, which will be initialized after the callback if no value is provided.
    /// The string parameter represents the destination URI prefix that should be used when constructing the RequestUri.
    /// The headers will be copied before the callback if CopyRequestHeaders is enabled.
    /// </summary>
    public Task OnRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest) => Default.OnRequestAsync(httpContext, proxyRequest);

    /// <summary>
    /// A callback that is invoked when the proxied response is received. The status code and reason phrase will be copied
    /// to the HttpContext.Response before the callback is invoked, but may still be modified there. The headers will be
    /// copied to HttpContext.Response.Headers before the callback if CopyResponseHeaders is enabled.
    /// </summary>
    public virtual Task OnResponseAsync(HttpContext httpContext, HttpResponseMessage proxyResponse) => Default.OnResponseAsync(httpContext, proxyResponse);

    /// <summary>
    /// A callback that is invoked after the response body to modify trailers, if supported. The trailers will be
    /// copied to the HttpContext.Response before the callback if CopyResponseTrailers is enabled.
    /// </summary>
    public virtual Task OnResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse) => Default.OnResponseTrailersAsync(httpContext, proxyResponse);
}

@Tratcher
Copy link
Member Author

Tratcher commented Dec 15, 2020

@davidfowl Why abstract + Default instead of having the defaults directly in a virtual base class? As a way of communicating to people that they must derive from it to change anything? Would a protected constructor accomplish the same?

@Tratcher Tratcher added this to the YARP 1.0.0-preview8 milestone Dec 16, 2020
@Tratcher Tratcher marked this pull request as ready for review December 16, 2020 01:23
}


private static void CopyResponseHeaders(HttpHeaders source, IHeaderDictionary destination)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we make this protected static or is it simple enough to copy around

Copy link
Member Author

Choose a reason for hiding this comment

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

If you're going to customize TransformResponseAsync or TransformResponseTrailersAsync enough not to call the base implementation then you probably need to customize this code too. That's what happened for the structured transforms derivation.

{
// TODO: this list only contains "Transfer-Encoding" because that messes up Kestrel. If we don't need to add any more here then it would be more efficient to
// check for the single value directly. What about connection headers?
internal static readonly HashSet<string> ResponseHeadersToSkip = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
Copy link
Contributor

Choose a reason for hiding this comment

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

This may as well be an array.

Copy link
Member Author

Choose a reason for hiding this comment

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

I expect this logic to get more interesting when we fix #583. I'll hold off changes until then.

/// <param name="query">The query to append</param>
internal static Uri MakeDestinationAddress(string destinationPrefix, PathString path, QueryString query)
{
var builder = new StringBuilder(destinationPrefix);
Copy link
Contributor

Choose a reason for hiding this comment

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

Allocations in the hot path.

Copy link
Member Author

@Tratcher Tratcher Dec 18, 2020

Choose a reason for hiding this comment

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

Yes, but it's trying to save you from even more allocations 😁. Let's revisit this on its own when we get to profiling.

@davidfowl
Copy link
Contributor

@davidfowl Why abstract + Default instead of having the defaults directly in a virtual base class? As a way of communicating to people that they must derive from it to change anything? Would a protected constructor accomplish the same?

Yea you're right.

@Tratcher
Copy link
Member Author

@davidfowl I think everybody else is out now, consider yourself the sole reviewer.

@davidfowl
Copy link
Contributor

Do we still want to (eventually) provide an API that makes this easy to do without overriding the IHttpProxy? Something you can do in middleware?

@Tratcher
Copy link
Member Author

Do we still want to (eventually) provide an API that makes this easy to do without overriding the IHttpProxy? Something you can do in middleware?

That's where #60 comes in.

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

Successfully merging this pull request may close these issues.

2 participants