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

Escaped Commas cannot be used in a filter string #444

Closed
mary-gao opened this issue Nov 9, 2018 · 9 comments · Fixed by #792
Closed

Escaped Commas cannot be used in a filter string #444

mary-gao opened this issue Nov 9, 2018 · 9 comments · Fixed by #792

Comments

@mary-gao
Copy link

mary-gao commented Nov 9, 2018

Description

It's not possible right now to search for items using a comma in the filter string. So, when I GET

/entities?filter[name]=like:%2C

I don't get the entries that I expect (all the entities with a name that contains a comma).

It looks like this is because we split the filter string on the comma after it has already been decoded, so we do not distinguish between "?filter[name]=a%2Cc" and "?filter[name]=a,c".

Environment

  • JsonApiDotNetCore Version: 2.4.0
@jaredcnance
Copy link
Contributor

jaredcnance commented Nov 10, 2018

🤔 I'm not sure that we should distinguish between encoded and non encoded commas (or even if we can—if i remember correctly, it's already decoded when we get it). This behavior comes from the JSON:API recommendations on filtering: https://jsonapi.org/recommendations/#filtering/

But, maybe we should add a flag to opt out of comma separated value parsing. Would that work for your needs? And if so, would you use it app-wide or on a per resource basis?

@mary-gao
Copy link
Author

I was hoping to use comma separated value parsing as well, actually. I noticed that the url was decoded already by the time the query parsing happened, but I don't know why that is. Couldn't we get the unescaped string directly from the request in the controller, and then split the query parameters, and then decode the query parameters?

@ScottEDubya
Copy link

I don't think the jsonapi spec specifically says anything about what you should/shouldn't be able to filter on. if i wanted to filter on multiple things with commas in their names, my url should look like the following:
http://domain.com/entities?filter[name]=foo%2C%20bar,boo%2C%20far
so we could search for two names "foo, bar" and "boo, far". there's a distinguishable difference in the literal URL between the commas for value separation and the commas which we're looking for in our filter.

i think this is related/under the hood nearly the same issue as the one i opened a bit ago: #408
the way the library is handling the url (or possibly allowing .net core to do the url parsing in its default way maybe?) is limiting our ability to effectively filter our data sets

@jaredcnance
Copy link
Contributor

jaredcnance commented Nov 12, 2018

I think that's an accurate assessment. What we'll need to figure out is how to get the raw url encoded values since it looks like AspNetCore already decodes them when deserializing to string. Right now we're just passing in the HttpContext.Request.Query. We can probably get them directly out of the request but may need to do our own parsing. Or we may be able to inject our own formatter, will have to do some research.

@ScottEDubya
Copy link

Yeah, @mary-gao seems to have found that the controllers do have access to the literal query string, but as far as it being the right solution to write jsonapi's own parser, i'm not sure about that one. feels like something someone in the past would have run into and there might already be an elegant solution out there!

@jaredcnance
Copy link
Contributor

jaredcnance commented Nov 13, 2018

In order to implement this, we will need to use HttpContext.Request.QueryString instead of HttpContext.Request.Query. The difference here is that QueryString contains the raw string value prior to parsing. But, that means we'll need to split the query string on & and = ourselves. This isn't great because that means AspNetCore will be doing unnecessary work and allocations when parsing and constructing the QueryCollection. So, then for every request the path is stored in at least 3 different locations in memory (but probably 4 since it's probably accessible through the Location header as well).

image

It would be a lot better if we could hijack the parsing work so it only happens once and leaves the values percent encoded. I did some quick looking but haven't found where the QueryCollection actually gets built in the AspNetCore pipeline. I looked at implementing a custom IValueProviderFactory, but by the time it is called, it looks like the QueryCollection has already been created.

@jaredcnance
Copy link
Contributor

jaredcnance commented Nov 14, 2018

Digging a little further this is where it happens:

So, based on that we'd need a way to provide a custom HttpRequest implementation that inherits from DefaultHttpRequest and just changes the query parsing behavior. Since the DefaultHttpRequest is instantiated directly by the DefaultHttpContext rather than using an injected factory (https://github.com/aspnet/HttpAbstractions/blob/1d3521113ad69a9903d1590c77d67da04c5f5dfd/src/Microsoft.AspNetCore.Http/DefaultHttpContext.cs#L196), it seems like we'd need to create our own implementation of HttpRequest, HttpContext and IHttpContextAccessor. So, I'm leaning towards parsing the QueryString ourselves.

As a side note this would be a breaking change since I suspect some clients are already encoding , as %2C and using it as an &key=value2 replacement.

@roblankey
Copy link
Contributor

Alternatively, what about just using a CSV-type parser and surrounding filter values containing commas with quotes. CsvHelper and TinyCsvParser are the two that jumped out pretty quickly when searching.

@jaredcnance
Copy link
Contributor

jaredcnance commented Nov 26, 2018

I don't think we need to bring a dependency in for this. Parsing is not difficult and I don't think it should be specific to %2C. I just didn't want to do extra work on each request if we didn't need to. I think to avoid a breaking change and unexpected behavior for existing users we can:

  • Introduce an IQueryParamParser:
public interface IQueryParamParser {
  QueryCollection GetParsedQueryParams();
}
  • Add a default implementation that forwards the already parsed/cached values
public class DefaultQueryParamParser : IQueryParamParser {
  private readonly HttpContext _httpContext
  public DefaultQueryParamParser(IHttpContextAccessor httpContextAccessor) {
     _httpContext = httpContextAccessor.HttpContext;
  }
  public QueryCollection GetParsedQueryParams() => _httpContext.Request.Query;
}
  • Add optional implementation that doesn't decode values
// better name desired...
public class RawValueQueryParamParser : IQueryParamParser { /* TBD */ }
  • Provide either a strategy enum on JsonApiOptions or just require the user to inject the service.
services.AddJsonApi(options => {
  options.QueryParsingStrategy = QueryParsingStrategy.KeepRawValues;
});

services.AddScoped<RawValueQueryParamParser, DefaultQueryParamParser>();

In a future major release we might make this the default behavior. I suspect it will be necessary to address #408.

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

Successfully merging a pull request may close this issue.

4 participants