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

update HttpRequestKernel to use new parser; support variable expressions in request body #3122

Merged

Conversation

jonsequitur
Copy link
Contributor

No description provided.

@jonsequitur jonsequitur marked this pull request as ready for review August 9, 2023 01:24
@jonsequitur jonsequitur enabled auto-merge (squash) August 9, 2023 18:29

public static AndWhichConstraint<ObjectAssertions, T> ContainSingle<T>(
this GenericCollectionAssertions<HttpSyntaxNodeOrToken> should)
where T : HttpSyntaxNode
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we remove the overload on line 13 since HttpSyntaxNodeOrToken is a base type of HttpSyntaxNode (and since IReadOnlyList supports covariance)?

/// <summary>
/// Gets the text of the current node, including trivia.
/// </summary>
public string FullText => SourceText.ToString(FullSpan);
Copy link
Contributor

@shyamnamboodiripad shyamnamboodiripad Aug 9, 2023

Choose a reason for hiding this comment

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

It is a bit strange that FullSpan is available on the base HttpSyntaxNodeOrToken but FullText is not.

If we think that FullSpan and FullText are only meaningful for HttpSyntaxNode, perhaps we should also move FullSpan down to HttpSyntaxNode.

Alternatively, we can move FullText up to the base type so that a caller does not have to think about whether they are dealing a node or a token when they want to get its full text.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This makes sense, but the meaning of the two is also different. On a token, Span really means the full span. FullSpan is used for things like FindNode and GrowSpan. Removing HttpSyntaxNodeOrToken.FullSpan results in needing to switch on the two types in a number of places.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see... Should we also move FullText to the base type then so that the API is consistent?

@jonsequitur jonsequitur merged commit a554838 into dotnet:main Aug 10, 2023
4 checks passed
@@ -70,10 +74,9 @@ internal class HttpRequestNode : HttpSyntaxNode
public HttpBindingResult<HttpRequestMessage> TryGetHttpRequestMessage(HttpBindingDelegate bind)
{
var request = new HttpRequestMessage();
var diagnostics = new List<Diagnostic>();
var success = true;
var diagnostics = new List<Diagnostic>(base.GetDiagnostics());
Copy link
Contributor

@shyamnamboodiripad shyamnamboodiripad Aug 10, 2023

Choose a reason for hiding this comment

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

The binding methods for the child nodes (such as TryGetUri and TryGetBody) should probably also call base.GetDiagnostics() to include syntax errors (and return false if there are any diagnostics with severity Error).

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess that could lead to some duplication at higher levels (unless we change the list to a hashset)...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I deliberately kept these concerns separate.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thinking more perhaps it may be better for the binding API calls to only return binding errors. The syntax errors are already available on the node which the caller is trying to bind.

This may be more flexible from an API standpoint and would also keep the behavior consistent regardless of which construct is being bound... Otherwise, it forces callers to reason about which construct they are binding and whether or not they should fetch the syntax diagnostics separately.

diagnostics.AddRange(uriBindingResult.Diagnostics);
}

if (success)
var headers =
HeadersNode?.HeaderNodes.Select(h => new KeyValuePair<string, string>(h.NameNode.Text, h.ValueNode.Text)).ToArray()
Copy link
Contributor

Choose a reason for hiding this comment

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

The headers could also contain expressions. But I guess we will flesh that out in future PRs cc @bleaphar

case "content-type":
if (request.Content is null)
{
request.Content = new StringContent("");
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this be done globally in the else clause for the if block on line 136 below instead (i.e. always set to empty string if a parsed body is empty)?

The null check for request.Content is also confusing since we have not touched request.Content above this line. Did you mean to check something else perhaps?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I meant to check this. Coverage indicates that request.Content is null 100% of the time here. But I realized there's a bug in that this header can be get overridden when we set the Content again for the body below, so some refactoring is needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah does the System.Net.Http API try to deduce and set the content-type header implicitly when Content is set below? That is certainly not very intuitive 😄

I agree this needs some refactoring... I think we can delete this if block for now as it is dead code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The code isn't dead code. 100% of tests enter the if block.

Ah does the System.Net.Http API try to deduce and set the content-type header implicitly when Content is set below?

It doesn't and there's no way it reliably could.

.FirstOrDefault(n => n.IsSignificant);

var lastSignificantNodeOrToken = ChildNodesAndTokens
.LastOrDefault(n => n.IsSignificant);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can improve this part by remembering the first and last significant children in fields as children are added inside Add below. That way we won't need to traverse all the child nodes here each time a child is added.

var parsedRequests = new List<ParsedHttpRequest>();

foreach (var (request, diagnostics) in InterpolateAndGetDiagnostics(requests))
foreach (var expressionNode in parseResult.SyntaxTree.RootNode.DescendantNodesAndTokensAndSelf().OfType<HttpExpressionNode>())
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah why not just call parseResult.TryGetRequest() and get the diagnostics that way (and throw away the request in the success case)? It seems a bit strange to have to repeat the binding logic here + keeping the set of diagnostics the same for both code paths may prove cumbersome.

For example, we may add more specific diagnostics for bad URIs / bad header declations inside the .TryGetUri / TryGetHeaders code path. Some of those diagnostics may be reported in URIs / headers with no embedded expressions (but would still be diagnostics that are reported at bind-time). It would be difficult to replicate the same diagnostics over here since this code only tries to bind embedded expressions (but does not bind individual parts of the syntax).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Some of those diagnostics may be reported in URIs / headers with no embedded expressions (but would still be diagnostics that are reported at bind-time).

The block before this (parseResult.GetDiagnostics()) is getting the syntactic diagnostics and this code is getting the symbolic diagnostics. My thinking was that keeping them as separate steps is useful in this code path (i.e. in the context of RequestDiagnostics) because the latter will sometimes include side effects such as UI prompts which we shouldn't trigger here.

Copy link
Contributor

Choose a reason for hiding this comment

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

What I meant is that there could be more to the symbolic diagnostics beyond variable resolution and this loop only seems to be handling variable resolution.

The TryGetRequest API (and other APIs it calls such as TryGetUri) would already be considering all such diagnostics - so my thinking was that it may be better to rely exclusively on that API and centralize all validation / binding logic over there.

Copy link
Contributor Author

@jonsequitur jonsequitur Aug 11, 2023

Choose a reason for hiding this comment

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

What I meant is that there could be more to the symbolic diagnostics beyond variable resolution and this loop only seems to be handling variable resolution.

Do we have an example of this?

My concern about binding those types to a specific API call is that it's also not necessarily correct. TryGetHttpRequest isn't the only possible use of the diagnostics request, so using it here feels odd from a layering perspective.

Copy link
Contributor

@shyamnamboodiripad shyamnamboodiripad Aug 14, 2023

Choose a reason for hiding this comment

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

One example may be if we add validation for supported version numbers for HttpVersionNode. Another may be if we need to add validation for specific values that are supported / unsupported for HeaderValueNode depending on the HeaderNameNode of the corresponding header...

Wonder if we should try and create a more general TryBind() API which performs the same binding steps that TryGetHttpRequest() does without creating / returning the System.Net.Http.* objects. However, I feel some of these validations may require calling into System.Net.Http regardless so we may not be saving much...

Copy link
Contributor Author

@jonsequitur jonsequitur Aug 15, 2023

Choose a reason for hiding this comment

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

I would expect these to be directly handled by the parser, since they don't change often and they don't vary based on the host environment. These are closer to syntactic than to symbolic validations.

Copy link
Contributor

@shyamnamboodiripad shyamnamboodiripad left a comment

Choose a reason for hiding this comment

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

@jonsequitur The changes mostly look great 👍🏾

I've added a few comments that may be worth a look. Some of those (such as a potential perf improvement for GrowSpan()) may be worth fixing up in follow up PRs.

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