From de7d49d541436fc3299e05981c2528e4dd1446b0 Mon Sep 17 00:00:00 2001 From: Ching Cheng Kang Date: Thu, 7 May 2026 18:14:40 +0800 Subject: [PATCH 1/2] Fix: Don't pre-urlencode Postman collection query keys/values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the Postman Collection v2.1 schema, `query[].key` and `query[].value` are stored as raw strings — Postman encodes them when assembling the outgoing request. Pre-encoding here causes a double-encode on send (e.g. a comma in `?include=customer,reward` becomes `%2C` in the collection, then `%252C` when Postman sends), which trips request parsers like Spatie QueryBuilder that split on a literal `,`. URL parameter `value`s had the same issue. The `raw` URL field still needs to be a syntactically valid URL string, so `rawurlencode()` is applied only when assembling that string. Adds a regression test covering comma + bracket + space cases. --- src/Writing/PostmanCollectionWriter.php | 13 +++-- tests/Unit/PostmanCollectionWriterTest.php | 58 ++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/Writing/PostmanCollectionWriter.php b/src/Writing/PostmanCollectionWriter.php index ea386e0f..445595a5 100644 --- a/src/Writing/PostmanCollectionWriter.php +++ b/src/Writing/PostmanCollectionWriter.php @@ -318,8 +318,8 @@ protected function generateUrlObject(OutputEndpointData $endpointData): array } } else { $query[] = [ - 'key' => urlencode($name), - 'value' => $parameterData->example !== null ? urlencode($parameterData->example) : '', + 'key' => $name, + 'value' => $parameterData->example !== null ? (string) $parameterData->example : '', 'description' => strip_tags($parameterData->description), // Default query params to disabled if they aren't required and have empty values 'disabled' => ! $parameterData->required && empty($parameterData->example), @@ -329,9 +329,12 @@ protected function generateUrlObject(OutputEndpointData $endpointData): array $base['query'] = $query; - // Create raw url-parameter (Insomnia uses this on import) + // Create raw url-parameter (Insomnia uses this on import). + // Per Postman Collection v2.1, query[].key/value are stored raw and the client + // encodes them when sending. The raw URL string still needs encoding to be a + // syntactically valid URL, so we only encode it here. $queryString = collect($base['query'])->map(function ($queryParamData) { - return $queryParamData['key'].'='.$queryParamData['value']; + return rawurlencode($queryParamData['key']).'='.rawurlencode($queryParamData['value']); })->implode('&'); $base['raw'] = sprintf('%s/%s%s', $base['host'], $base['path'], $queryString ? "?{$queryString}" : null); @@ -344,7 +347,7 @@ protected function generateUrlObject(OutputEndpointData $endpointData): array return [ 'id' => $name, 'key' => $name, - 'value' => urlencode($parameter->example), + 'value' => $parameter->example, 'description' => $parameter->description, ]; })->values()->toArray(); diff --git a/tests/Unit/PostmanCollectionWriterTest.php b/tests/Unit/PostmanCollectionWriterTest.php index 1a898045..82665410 100644 --- a/tests/Unit/PostmanCollectionWriterTest.php +++ b/tests/Unit/PostmanCollectionWriterTest.php @@ -207,6 +207,64 @@ public function query_parameters_are_disabled_with_no_value_when_not_required() ], $variableData); } + /** @test */ + public function query_parameter_keys_and_values_are_not_pre_url_encoded() + { + // Per Postman Collection v2.1 spec, query[].key/value are stored as raw strings + // and the Postman client encodes them when sending. Pre-encoding here causes + // a double-encode on send (e.g. `,` → `%2C` → `%252C` at the server). + $endpointData = $this->createMockEndpointData('fake/{id}'); + $endpointData->urlParameters['id'] = new Parameter([ + 'name' => 'id', + 'description' => '', + 'required' => true, + 'example' => 'foo bar', + ]); + $endpointData->queryParameters = [ + 'include' => new Parameter([ + 'name' => 'include', + 'type' => 'string', + 'description' => 'Comma-separated relationships', + 'required' => false, + 'example' => 'customer,reward', + ]), + 'filter[email]' => new Parameter([ + 'name' => 'filter[email]', + 'type' => 'string', + 'description' => 'Filter', + 'required' => false, + 'example' => 'test@example.com', + ]), + ]; + $endpointData->cleanQueryParameters = Extractor::cleanParams($endpointData->queryParameters); + + $endpoints = $this->createMockEndpointGroup([$endpointData]); + $collection = $this->generate(endpoints: [$endpoints]); + + $url = data_get($collection, 'item.0.item.0.request.url'); + + // Query objects keep raw values — Postman encodes on send. + $this->assertContains([ + 'key' => 'include', + 'value' => 'customer,reward', + 'description' => 'Comma-separated relationships', + 'disabled' => false, + ], $url['query']); + $this->assertContains([ + 'key' => 'filter[email]', + 'value' => 'test@example.com', + 'description' => 'Filter', + 'disabled' => false, + ], $url['query']); + + // URL variables also stored raw. + $this->assertSame('foo bar', $url['variable'][0]['value']); + + // Raw URL is encoded for HTTP validity. + $this->assertStringContainsString('include=customer%2Creward', $url['raw']); + $this->assertStringContainsString('filter%5Bemail%5D=test%40example.com', $url['raw']); + } + /** @test */ public function auth_info_is_added_correctly() { From 0daf9d02e4c3f41eb0a280d8a55bbedffb26f96b Mon Sep 17 00:00:00 2001 From: Ching Cheng Kang Date: Fri, 8 May 2026 11:53:12 +0800 Subject: [PATCH 2/2] Update Postman fixture to match rawurlencode + decoded query values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixture stored a pre-encoded query value and a `+`-style space in the raw URL. After de7d49d5, the writer keeps key/value pairs decoded and renders the raw URL via rawurlencode (RFC 3986 — `%20` for space). Fixture refreshed accordingly. --- tests/Fixtures/collection.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Fixtures/collection.json b/tests/Fixtures/collection.json index d8ad8b13..6451629e 100644 --- a/tests/Fixtures/collection.json +++ b/tests/Fixtures/collection.json @@ -170,12 +170,12 @@ }, { "key": "url_encoded", - "value": "%2B+%5B%5D%26%3D", + "value": "+ []&=", "description": "Used for testing that URL parameters will be URL-encoded where needed.", "disabled": false } ], - "raw": "{{baseUrl}}/api/withQueryParameters?location_id=architecto&user_id=me&page=4&filters=architecto&url_encoded=%2B+%5B%5D%26%3D" + "raw": "{{baseUrl}}/api/withQueryParameters?location_id=architecto&user_id=me&page=4&filters=architecto&url_encoded=%2B%20%5B%5D%26%3D" }, "method": "GET", "header": [