Skip to content

Comments

Fix query string contents in nextLink for REST (remove dupe $after query parameter)#2006

Merged
seantleonard merged 5 commits intorelease/0.10from
dev/sean/wf_nextlink_dupe_qpafter
Feb 5, 2024
Merged

Fix query string contents in nextLink for REST (remove dupe $after query parameter)#2006
seantleonard merged 5 commits intorelease/0.10from
dev/sean/wf_nextlink_dupe_qpafter

Conversation

@seantleonard
Copy link
Contributor

@seantleonard seantleonard commented Feb 2, 2024

Fix query string contents in nextLink for REST

Why?

The change committed in #1895 was written to fix how DAB writes the nextLink value in a response body to a GET request using pagination.
The nextLink contents is a URL which includes a query parameter $after which contains base64 encoded pagination metadata.
#1895 inadvertently triggered the nextLink creation process to append an additional $after query parameter instead of overwriting the
$after query parameter present in the request.

Sample request:

GET https://localhost:5001/api/Bookmarks?$first=1

Response:

{
    "value": [
        {
            "id": 1,
            "bkname": "Test Item #00001"
        }
    ],
    "nextLink": "https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0="
}

The $after query parameter's value is a properly formatted base64 encoded string as fixed in #1895. The value is an opaque string and
from the above sample response translates to:

[{"EntityName":"Bookmarks","FieldName":"id","FieldValue":1,"Direction":0}]

However, once the nextLink is used to fetch another page, the next page's results include an invalid response body because
the nextLink is improperly formed:

GET https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0=
{
    "value": [
        {
            "id": 2,
            "bkname": "Test Item #00002"
        }
    ],
    "nextLink": "https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0%3D&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MiwiRGlyZWN0aW9uIjowfV0="
}

The invalid and and unexpected value is: $after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0%3D
Not only is it URL escaped, but it is a duplicate value that shouldn't be present.

What is this change?

This change essentially removes the old $after query parameter (key and value) from the queryStringParameters
NamedValueCollection passed in to CreateNextLink(...):

public static JsonElement CreateNextLink(string path, NameValueCollection? queryStringParameters, string after){}

Due the NamedValueCollection being a special object created by System.Web.HttpUtility.HttpQSCollection

The ParseQueryString method uses UTF8 format to parse the query string In the returned NameValueCollection, URL encoded characters are decoded and multiple occurrences of the same query string parameter are listed as a single entry with a comma separating each value.

The values are URI escaped and this change purges that uri escaped value:

// Purge old $after value so this function can replace it.
queryStringParameters.Remove("$after");
string queryString = FormatQueryString(queryStringParameters: queryStringParameters);
if (!string.IsNullOrWhiteSpace(after))
{
    string afterPrefix = string.IsNullOrWhiteSpace(queryString) ? "?" : "&";
    queryString += $"{afterPrefix}{RequestParser.AFTER_URL}={after}";
}

The above code:

  1. Creates an initial queryString where the values are URL encoded.
  2. Checks if a new after link is available to inject into query params
  3. APPENDS a NEW and unique $after query param. (old code simply appended an $after query parameter even if one already existed.)
  4. JSON Serialize and then JSON Deserialize probably to get rid of some sort of unexpected formatting.

How was this change tested?

Added integration test to ensure nextLink creation doesn't regress and to exercise acquiring
the nextLink from subsequent pages. The test setup makes an initial request formed to invoke DAB to return
a nextLink in the response. The assertions occur on the 2nd request which uses the nextLink returned from
the first request:

  1. Response is 200 -> 400 would indicate issue with query parameter payload
  2. Ensures nextLink value different from the value returned on the first request used during test setup -> ensures
    DAB is not recycling a single $after query parameter/nextLink value.
  3. Ensures nextLink query parameter $after does not have a comma, which would indicate that parsing of the query
    string detected two instance of the $after query parameter, which per dotnet behavior, concatenates the values and separates
    them with a comma.
  4. Ensure $after value is base64encoded and not URI encoded.

Sample request

As illustrated earlier:

GET https://localhost:5001/api/Bookmarks?$first=1

Use the nextLink returned to send a follow up pagination request:

GET https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0=

@seantleonard
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 3 pipeline(s).

Copy link
Collaborator

@Aniruddh25 Aniruddh25 left a comment

Choose a reason for hiding this comment

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

LGTM, Thank you so much for the quick fix, and the comprehensive tests

…gination as long as $first query param is used with low value.
Copy link
Contributor

@abhishekkumams abhishekkumams left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@severussundar severussundar left a comment

Choose a reason for hiding this comment

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

Thank you for the fix :)

@seantleonard seantleonard added this to the 0.11rc milestone Feb 5, 2024
@seantleonard
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@seantleonard seantleonard merged commit 126737d into release/0.10 Feb 5, 2024
@seantleonard seantleonard deleted the dev/sean/wf_nextlink_dupe_qpafter branch February 5, 2024 22:22
seantleonard added a commit that referenced this pull request Feb 6, 2024
…query parameter) (#2006)

# Fix query string contents in nextLink for REST

## Why?

The change committed in #1895 was written to fix how DAB writes the
`nextLink` value in a response body to a GET request using pagination.
The `nextLink` contents is a URL which includes a query parameter
`$after` which contains base64 encoded pagination metadata.
#1895 inadvertently triggered the `nextLink` creation process to append
an additional `$after` query parameter instead of overwriting the
`$after` query parameter present in the request.

Sample request:

```https
GET https://localhost:5001/api/Bookmarks?$first=1
```

Response:

```json
{
    "value": [
        {
            "id": 1,
            "bkname": "Test Item #1"
        }
    ],
    "nextLink": "https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0="
}
```

The `$after` query parameter's value is a properly formatted base64
encoded string as fixed in #1895. The value is an opaque string and
from the above sample response translates to:

```json
[{"EntityName":"Bookmarks","FieldName":"id","FieldValue":1,"Direction":0}]
```

However, once the `nextLink` is used to fetch another page, the next
page's results include an invalid response body because
the `nextLink` is improperly formed:

```https
GET https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0=
```

```json
{
    "value": [
        {
            "id": 2,
            "bkname": "Test Item #2"
        }
    ],
    "nextLink": "https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0%3D&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MiwiRGlyZWN0aW9uIjowfV0="
}
```

The invalid and and unexpected value is:
`$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0%3D`
Not only is it URL escaped, but it is a duplicate value that shouldn't
be present.

## What is this change?

This change essentially removes the old `$after` query parameter (key
and value) from the queryStringParameters
NamedValueCollection passed in to `CreateNextLink(...)`:

```csharp
public static JsonElement CreateNextLink(string path, NameValueCollection? queryStringParameters, string after){}
```

Due the NamedValueCollection being a special object created by
`System.Web.HttpUtility.HttpQSCollection`
> The ParseQueryString method uses UTF8 format to parse the query string
In the returned NameValueCollection, URL encoded characters are decoded
and multiple occurrences of the same query string parameter are listed
as a single entry with a comma separating each value.

The values are URI escaped and this change purges that uri escaped
value:

```csharp
// Purge old $after value so this function can replace it.
queryStringParameters.Remove("$after");
```

```csharp
string queryString = FormatQueryString(queryStringParameters: queryStringParameters);
if (!string.IsNullOrWhiteSpace(after))
{
    string afterPrefix = string.IsNullOrWhiteSpace(queryString) ? "?" : "&";
    queryString += $"{afterPrefix}{RequestParser.AFTER_URL}={after}";
}
```

The above code:
1. Creates an initial `queryString` where the values are URL encoded.
2. Checks if a new after link is available to inject into query params
3. APPENDS a NEW and unique `$after` query param. (old code simply
appended an `$after` query parameter even if one already existed.)
4. JSON Serialize and then JSON Deserialize probably to get rid of some
sort of unexpected formatting.

## How was this change tested?

Added integration test to ensure nextLink creation doesn't regress and
to exercise acquiring
the `nextLink` from subsequent pages. The test setup makes an initial
request formed to invoke DAB to return
a `nextLink` in the response. The assertions occur on the **2nd
request** which uses the `nextLink` returned from
the first request:

1. Response is 200 -> 400 would indicate issue with query parameter
payload
1. Ensures `nextLink` value different from the value returned on the
first request used during test setup -> ensures
DAB is not recycling a single $after query parameter/nextLink value.
1. Ensures `nextLink` query parameter `$after` does not have a comma,
which would indicate that parsing of the query
string detected two instance of the `$after` query parameter, which per
dotnet behavior, concatenates the values and separates
them with a comma.
1. Ensure `$after` value is base64encoded and not URI encoded.

## Sample request

As illustrated earlier:

```https
GET https://localhost:5001/api/Bookmarks?$first=1
```
Use the `nextLink` returned to send a follow up pagination request:

```https
GET https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0=
```
seantleonard added a commit that referenced this pull request Feb 6, 2024
…query parameter) (#2006)

# Fix query string contents in nextLink for REST

## Why?

The change committed in #1895 was written to fix how DAB writes the
`nextLink` value in a response body to a GET request using pagination.
The `nextLink` contents is a URL which includes a query parameter
`$after` which contains base64 encoded pagination metadata.
#1895 inadvertently triggered the `nextLink` creation process to append
an additional `$after` query parameter instead of overwriting the
`$after` query parameter present in the request.

Sample request:

```https
GET https://localhost:5001/api/Bookmarks?$first=1
```

Response:

```json
{
    "value": [
        {
            "id": 1,
            "bkname": "Test Item #1"
        }
    ],
    "nextLink": "https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0="
}
```

The `$after` query parameter's value is a properly formatted base64
encoded string as fixed in #1895. The value is an opaque string and
from the above sample response translates to:

```json
[{"EntityName":"Bookmarks","FieldName":"id","FieldValue":1,"Direction":0}]
```

However, once the `nextLink` is used to fetch another page, the next
page's results include an invalid response body because
the `nextLink` is improperly formed:

```https
GET https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0=
```

```json
{
    "value": [
        {
            "id": 2,
            "bkname": "Test Item #2"
        }
    ],
    "nextLink": "https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0%3D&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MiwiRGlyZWN0aW9uIjowfV0="
}
```

The invalid and and unexpected value is:
`$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0%3D`
Not only is it URL escaped, but it is a duplicate value that shouldn't
be present.

## What is this change?

This change essentially removes the old `$after` query parameter (key
and value) from the queryStringParameters
NamedValueCollection passed in to `CreateNextLink(...)`:

```csharp
public static JsonElement CreateNextLink(string path, NameValueCollection? queryStringParameters, string after){}
```

Due the NamedValueCollection being a special object created by
`System.Web.HttpUtility.HttpQSCollection`
> The ParseQueryString method uses UTF8 format to parse the query string
In the returned NameValueCollection, URL encoded characters are decoded
and multiple occurrences of the same query string parameter are listed
as a single entry with a comma separating each value.

The values are URI escaped and this change purges that uri escaped
value:

```csharp
// Purge old $after value so this function can replace it.
queryStringParameters.Remove("$after");
```

```csharp
string queryString = FormatQueryString(queryStringParameters: queryStringParameters);
if (!string.IsNullOrWhiteSpace(after))
{
    string afterPrefix = string.IsNullOrWhiteSpace(queryString) ? "?" : "&";
    queryString += $"{afterPrefix}{RequestParser.AFTER_URL}={after}";
}
```

The above code:
1. Creates an initial `queryString` where the values are URL encoded.
2. Checks if a new after link is available to inject into query params
3. APPENDS a NEW and unique `$after` query param. (old code simply
appended an `$after` query parameter even if one already existed.)
4. JSON Serialize and then JSON Deserialize probably to get rid of some
sort of unexpected formatting.

## How was this change tested?

Added integration test to ensure nextLink creation doesn't regress and
to exercise acquiring
the `nextLink` from subsequent pages. The test setup makes an initial
request formed to invoke DAB to return
a `nextLink` in the response. The assertions occur on the **2nd
request** which uses the `nextLink` returned from
the first request:

1. Response is 200 -> 400 would indicate issue with query parameter
payload
1. Ensures `nextLink` value different from the value returned on the
first request used during test setup -> ensures
DAB is not recycling a single $after query parameter/nextLink value.
1. Ensures `nextLink` query parameter `$after` does not have a comma,
which would indicate that parsing of the query
string detected two instance of the `$after` query parameter, which per
dotnet behavior, concatenates the values and separates
them with a comma.
1. Ensure `$after` value is base64encoded and not URI encoded.

## Sample request

As illustrated earlier:

```https
GET https://localhost:5001/api/Bookmarks?$first=1
```
Use the `nextLink` returned to send a follow up pagination request:

```https
GET https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0=
```
seantleonard added a commit that referenced this pull request Feb 12, 2024
…query parameter) (#2006)

# Fix query string contents in nextLink for REST

## Why?

The change committed in #1895 was written to fix how DAB writes the
`nextLink` value in a response body to a GET request using pagination.
The `nextLink` contents is a URL which includes a query parameter
`$after` which contains base64 encoded pagination metadata.
#1895 inadvertently triggered the `nextLink` creation process to append
an additional `$after` query parameter instead of overwriting the
`$after` query parameter present in the request.

Sample request:

```https
GET https://localhost:5001/api/Bookmarks?$first=1
```

Response:

```json
{
    "value": [
        {
            "id": 1,
            "bkname": "Test Item #1"
        }
    ],
    "nextLink": "https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0="
}
```

The `$after` query parameter's value is a properly formatted base64
encoded string as fixed in #1895. The value is an opaque string and
from the above sample response translates to:

```json
[{"EntityName":"Bookmarks","FieldName":"id","FieldValue":1,"Direction":0}]
```

However, once the `nextLink` is used to fetch another page, the next
page's results include an invalid response body because
the `nextLink` is improperly formed:

```https
GET https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0=
```

```json
{
    "value": [
        {
            "id": 2,
            "bkname": "Test Item #2"
        }
    ],
    "nextLink": "https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0%3D&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MiwiRGlyZWN0aW9uIjowfV0="
}
```

The invalid and and unexpected value is:
`$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0%3D`
Not only is it URL escaped, but it is a duplicate value that shouldn't
be present.

## What is this change?

This change essentially removes the old `$after` query parameter (key
and value) from the queryStringParameters
NamedValueCollection passed in to `CreateNextLink(...)`:

```csharp
public static JsonElement CreateNextLink(string path, NameValueCollection? queryStringParameters, string after){}
```

Due the NamedValueCollection being a special object created by
`System.Web.HttpUtility.HttpQSCollection`
> The ParseQueryString method uses UTF8 format to parse the query string
In the returned NameValueCollection, URL encoded characters are decoded
and multiple occurrences of the same query string parameter are listed
as a single entry with a comma separating each value.

The values are URI escaped and this change purges that uri escaped
value:

```csharp
// Purge old $after value so this function can replace it.
queryStringParameters.Remove("$after");
```

```csharp
string queryString = FormatQueryString(queryStringParameters: queryStringParameters);
if (!string.IsNullOrWhiteSpace(after))
{
    string afterPrefix = string.IsNullOrWhiteSpace(queryString) ? "?" : "&";
    queryString += $"{afterPrefix}{RequestParser.AFTER_URL}={after}";
}
```

The above code:
1. Creates an initial `queryString` where the values are URL encoded.
2. Checks if a new after link is available to inject into query params
3. APPENDS a NEW and unique `$after` query param. (old code simply
appended an `$after` query parameter even if one already existed.)
4. JSON Serialize and then JSON Deserialize probably to get rid of some
sort of unexpected formatting.

## How was this change tested?

Added integration test to ensure nextLink creation doesn't regress and
to exercise acquiring
the `nextLink` from subsequent pages. The test setup makes an initial
request formed to invoke DAB to return
a `nextLink` in the response. The assertions occur on the **2nd
request** which uses the `nextLink` returned from
the first request:

1. Response is 200 -> 400 would indicate issue with query parameter
payload
1. Ensures `nextLink` value different from the value returned on the
first request used during test setup -> ensures
DAB is not recycling a single $after query parameter/nextLink value.
1. Ensures `nextLink` query parameter `$after` does not have a comma,
which would indicate that parsing of the query
string detected two instance of the `$after` query parameter, which per
dotnet behavior, concatenates the values and separates
them with a comma.
1. Ensure `$after` value is base64encoded and not URI encoded. 


## Sample request

As illustrated earlier:

```https
GET https://localhost:5001/api/Bookmarks?$first=1
```
Use the `nextLink` returned to send a follow up pagination request:

```https
GET https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0=
```
Aniruddh25 added a commit that referenced this pull request Feb 13, 2024
…upe $after query parameter) (#2032)

Cherry picks #2006 to `main`

# Fix query string contents in nextLink for REST

## Why?

The change committed in #1895 was written to fix how DAB writes the
`nextLink` value in a response body to a GET request using pagination.
The `nextLink` contents is a URL which includes a query parameter
`$after` which contains base64 encoded pagination metadata.
#1895 inadvertently triggered the `nextLink` creation process to append
an additional `$after` query parameter instead of overwriting the
`$after` query parameter present in the request.

Sample request:

```https
GET https://localhost:5001/api/Bookmarks?$first=1
```

Response:

```json
{
    "value": [
        {
            "id": 1,
            "bkname": "Test Item #1"
        }
    ],
    "nextLink": "https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0="
}
```

The `$after` query parameter's value is a properly formatted base64
encoded string as fixed in #1895. The value is an opaque string and
from the above sample response translates to:

```json
[{"EntityName":"Bookmarks","FieldName":"id","FieldValue":1,"Direction":0}]
```

However, once the `nextLink` is used to fetch another page, the next
page's results include an invalid response body because
the `nextLink` is improperly formed:

```https
GET https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0=
```

```json
{
    "value": [
        {
            "id": 2,
            "bkname": "Test Item #2"
        }
    ],
    "nextLink": "https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0%3D&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MiwiRGlyZWN0aW9uIjowfV0="
}
```

The invalid and and unexpected value is:
`$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0%3D`
Not only is it URL escaped, but it is a duplicate value that shouldn't
be present.

## What is this change?

This change essentially removes the old `$after` query parameter (key
and value) from the queryStringParameters
NamedValueCollection passed in to `CreateNextLink(...)`:

```csharp
public static JsonElement CreateNextLink(string path, NameValueCollection? queryStringParameters, string after){}
```

Due the NamedValueCollection being a special object created by
`System.Web.HttpUtility.HttpQSCollection`
> The ParseQueryString method uses UTF8 format to parse the query string
In the returned NameValueCollection, URL encoded characters are decoded
and multiple occurrences of the same query string parameter are listed
as a single entry with a comma separating each value.

The values are URI escaped and this change purges that uri escaped
value:

```csharp
// Purge old $after value so this function can replace it.
queryStringParameters.Remove("$after");
```

```csharp
string queryString = FormatQueryString(queryStringParameters: queryStringParameters);
if (!string.IsNullOrWhiteSpace(after))
{
    string afterPrefix = string.IsNullOrWhiteSpace(queryString) ? "?" : "&";
    queryString += $"{afterPrefix}{RequestParser.AFTER_URL}={after}";
}
```

The above code:
1. Creates an initial `queryString` where the values are URL encoded.
2. Checks if a new after link is available to inject into query params
3. APPENDS a NEW and unique `$after` query param. (old code simply
appended an `$after` query parameter even if one already existed.)
4. JSON Serialize and then JSON Deserialize probably to get rid of some
sort of unexpected formatting.

## How was this change tested?

Added integration test to ensure nextLink creation doesn't regress and
to exercise acquiring
the `nextLink` from subsequent pages. The test setup makes an initial
request formed to invoke DAB to return
a `nextLink` in the response. The assertions occur on the **2nd
request** which uses the `nextLink` returned from
the first request:

1. Response is 200 -> 400 would indicate issue with query parameter
payload
1. Ensures `nextLink` value different from the value returned on the
first request used during test setup -> ensures
DAB is not recycling a single $after query parameter/nextLink value.
1. Ensures `nextLink` query parameter `$after` does not have a comma,
which would indicate that parsing of the query
string detected two instance of the `$after` query parameter, which per
dotnet behavior, concatenates the values and separates
them with a comma.
1. Ensure `$after` value is base64encoded and not URI encoded. 


## Sample request

As illustrated earlier:

```https
GET https://localhost:5001/api/Bookmarks?$first=1
```
Use the `nextLink` returned to send a follow up pagination request:

```https
GET https://localhost:5001/api/Bookmarks?$first=1&$after=W3siRW50aXR5TmFtZSI6IkJvb2ttYXJrcyIsIkZpZWxkTmFtZSI6ImlkIiwiRmllbGRWYWx1ZSI6MSwiRGlyZWN0aW9uIjowfV0=
```

Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants