Skip to content

Commit 797cea6

Browse files
authored
Merge pull request #2039 from jewome62/feature/better-http-headers
Better HTTP headers
2 parents 4032aa3 + cb26a49 commit 797cea6

File tree

10 files changed

+79
-14
lines changed

10 files changed

+79
-14
lines changed

features/main/crud.feature

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Feature: Create-Retrieve-Update-Delete
2222
Then the response status code should be 201
2323
And the response should be in JSON
2424
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
25+
And the header "Content-Location" should be equal to "/dummies/1"
26+
And the header "Location" should be equal to "/dummies/1"
2527
And the JSON should be equal to:
2628
"""
2729
{
@@ -426,6 +428,7 @@ Feature: Create-Retrieve-Update-Delete
426428
Then the response status code should be 200
427429
And the response should be in JSON
428430
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
431+
And the header "Content-Location" should be equal to "/dummies/1"
429432
And the JSON should be equal to:
430433
"""
431434
{
@@ -465,6 +468,7 @@ Feature: Create-Retrieve-Update-Delete
465468
Then the response status code should be 200
466469
And the response should be in JSON
467470
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
471+
And the header "Content-Location" should be equal to "/dummies/1"
468472
And the JSON should be equal to:
469473
"""
470474
{

features/main/crud_abstract.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Feature: Create-Retrieve-Update-Delete on abstract resource
1616
Then the response status code should be 201
1717
And the response should be in JSON
1818
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
19+
And the header "Content-Location" should be equal to "/concrete_dummies/1"
20+
And the header "Location" should be equal to "/concrete_dummies/1"
1921
And the JSON should be equal to:
2022
"""
2123
{
@@ -90,6 +92,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource
9092
Then the response status code should be 200
9193
And the response should be in JSON
9294
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
95+
And the header "Content-Location" should be equal to "/concrete_dummies/1"
9396
And the JSON should be equal to:
9497
"""
9598
{

features/main/custom_normalized.feature

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Feature: Using custom normalized entity
1616
Then the response status code should be 201
1717
And the response should be in JSON
1818
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
19+
And the header "Content-Location" should be equal to "/custom_normalized_dummies/1"
20+
And the header "Location" should be equal to "/custom_normalized_dummies/1"
1921
And the JSON should be equal to:
2022
"""
2123
{
@@ -40,6 +42,8 @@ Feature: Using custom normalized entity
4042
Then the response status code should be 201
4143
And the response should be in JSON
4244
And the header "Content-Type" should be equal to "application/json; charset=utf-8"
45+
And the header "Content-Location" should be equal to "/related_normalized_dummies/1"
46+
And the header "Location" should be equal to "/related_normalized_dummies/1"
4347
And the JSON should be equal to:
4448
"""
4549
{
@@ -68,6 +72,7 @@ Feature: Using custom normalized entity
6872
Then the response status code should be 200
6973
And the response should be in JSON
7074
And the header "Content-Type" should be equal to "application/json; charset=utf-8"
75+
And the header "Content-Location" should be equal to "/related_normalized_dummies/1"
7176
And the JSON should be equal to:
7277
"""
7378
{
@@ -134,6 +139,7 @@ Feature: Using custom normalized entity
134139
Then the response status code should be 200
135140
And the response should be in JSON
136141
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
142+
And the header "Content-Location" should be equal to "/custom_normalized_dummies/1"
137143
And the JSON should be equal to:
138144
"""
139145
{

features/main/custom_writable_identifier.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Feature: Using custom writable identifier on resource
1616
Then the response status code should be 201
1717
And the response should be in JSON
1818
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
19+
And the header "Content-Location" should be equal to "/custom_writable_identifier_dummies/my_slug"
20+
And the header "Location" should be equal to "/custom_writable_identifier_dummies/my_slug"
1921
And the JSON should be equal to:
2022
"""
2123
{
@@ -78,6 +80,7 @@ Feature: Using custom writable identifier on resource
7880
Then the response status code should be 200
7981
And the response should be in JSON
8082
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
83+
And the header "Content-Location" should be equal to "/custom_writable_identifier_dummies/slug_modified"
8184
And the JSON should be equal to:
8285
"""
8386
{

features/main/uuid.feature

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Feature: Using uuid identifier on resource
1616
Then the response status code should be 201
1717
And the response should be in JSON
1818
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
19+
And the header "Content-Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78"
20+
And the header "Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78"
1921

2022
Scenario: Get a resource
2123
When I send a "GET" request to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78"
@@ -67,6 +69,7 @@ Feature: Using uuid identifier on resource
6769
Then the response status code should be 200
6870
And the response should be in JSON
6971
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
72+
And the header "Content-Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78"
7073
And the JSON should be equal to:
7174
"""
7275
{
@@ -87,6 +90,8 @@ Feature: Using uuid identifier on resource
8790
Then the response status code should be 201
8891
And the response should be in JSON
8992
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
93+
And the header "Content-Location" should be equal to "/custom_generated_identifiers/foo"
94+
And the header "Location" should be equal to "/custom_generated_identifiers/foo"
9095
And the JSON should be equal to:
9196
"""
9297
{

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152

153153
<service id="api_platform.listener.view.write" class="ApiPlatform\Core\EventListener\WriteListener">
154154
<argument type="service" id="api_platform.data_persister" />
155+
<argument type="service" id="api_platform.iri_converter" on-invalid="null" />
155156

156157
<tag name="kernel.event_listener" event="kernel.view" method="onKernelView" priority="32" />
157158
</service>

src/EventListener/RespondListener.php

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,25 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
4040
return;
4141
}
4242

43+
$headers = [
44+
'Content-Type' => sprintf('%s; charset=utf-8', $request->getMimeType($request->getRequestFormat())),
45+
'Vary' => 'Accept',
46+
'X-Content-Type-Options' => 'nosniff',
47+
'X-Frame-Options' => 'deny',
48+
];
49+
50+
if ($request->attributes->has('_api_write_item_iri')) {
51+
$headers['Content-Location'] = $request->attributes->get('_api_write_item_iri');
52+
53+
if ($request->isMethod('POST')) {
54+
$headers['Location'] = $request->attributes->get('_api_write_item_iri');
55+
}
56+
}
57+
4358
$event->setResponse(new Response(
4459
$controllerResult,
4560
self::METHOD_TO_CODE[$request->getMethod()] ?? Response::HTTP_OK,
46-
[
47-
'Content-Type' => sprintf('%s; charset=utf-8', $request->getMimeType($request->getRequestFormat())),
48-
'Vary' => 'Accept',
49-
'X-Content-Type-Options' => 'nosniff',
50-
'X-Frame-Options' => 'deny',
51-
]
61+
$headers
5262
));
5363
}
5464
}

src/EventListener/WriteListener.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Core\EventListener;
1515

16+
use ApiPlatform\Core\Api\IriConverterInterface;
1617
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
1718
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
1819

@@ -25,10 +26,12 @@
2526
final class WriteListener
2627
{
2728
private $dataPersister;
29+
private $iriConverter;
2830

29-
public function __construct(DataPersisterInterface $dataPersister)
31+
public function __construct(DataPersisterInterface $dataPersister, IriConverterInterface $iriConverter = null)
3032
{
3133
$this->dataPersister = $dataPersister;
34+
$this->iriConverter = $iriConverter;
3235
}
3336

3437
/**
@@ -57,6 +60,10 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
5760
}
5861

5962
$event->setControllerResult($persistResult ?? $controllerResult);
63+
64+
if (null !== $this->iriConverter) {
65+
$request->attributes->set('_api_write_item_iri', $this->iriConverter->getIriFromItem($controllerResult));
66+
}
6067
break;
6168
case 'DELETE':
6269
$this->dataPersister->remove($controllerResult);

tests/EventListener/RespondListenerTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function testCreate201Response()
6767
{
6868
$kernelProphecy = $this->prophesize(HttpKernelInterface::class);
6969

70-
$request = new Request([], [], ['_api_respond' => true]);
70+
$request = new Request([], [], ['_api_respond' => true, '_api_write_item_iri' => '/dummy_entities/1']);
7171
$request->setMethod('POST');
7272
$request->setRequestFormat('xml');
7373

@@ -88,6 +88,8 @@ public function testCreate201Response()
8888
$this->assertEquals('Accept', $response->headers->get('Vary'));
8989
$this->assertEquals('nosniff', $response->headers->get('X-Content-Type-Options'));
9090
$this->assertEquals('deny', $response->headers->get('X-Frame-Options'));
91+
$this->assertEquals('/dummy_entities/1', $response->headers->get('Location'));
92+
$this->assertEquals('/dummy_entities/1', $response->headers->get('Content-Location'));
9193
}
9294

9395
public function testCreate204Response()

tests/EventListener/WriteListenerTest.php

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Core\Tests\EventListener;
1515

16+
use ApiPlatform\Core\Api\IriConverterInterface;
1617
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
1718
use ApiPlatform\Core\EventListener\WriteListener;
1819
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy;
@@ -35,6 +36,9 @@ public function testOnKernelViewWithControllerResultAndPersist()
3536
$dataPersisterProphecy->supports($dummy)->willReturn(true)->shouldBeCalled();
3637
$dataPersisterProphecy->persist($dummy)->willReturn($dummy)->shouldBeCalled();
3738

39+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
40+
$iriConverterProphecy->getIriFromItem($dummy)->willReturn('/dummy/1')->shouldBeCalled();
41+
3842
$request = new Request();
3943
$request->attributes->set('_api_resource_class', Dummy::class);
4044

@@ -48,8 +52,9 @@ public function testOnKernelViewWithControllerResultAndPersist()
4852
foreach (['PATCH', 'PUT', 'POST'] as $httpMethod) {
4953
$request->setMethod($httpMethod);
5054

51-
(new WriteListener($dataPersisterProphecy->reveal()))->onKernelView($event);
55+
(new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event);
5256
$this->assertSame($dummy, $event->getControllerResult());
57+
$this->assertEquals('/dummy/1', $request->attributes->get('_api_write_item_iri'));
5358
}
5459
}
5560

@@ -98,6 +103,9 @@ public function testOnKernelViewWithControllerResultAndPersistWithImmutableResou
98103
$dataPersisterProphecy = $this->prophesize(DataPersisterInterface::class);
99104
$dataPersisterProphecy->supports($dummy)->willReturn(true)->shouldBeCalled();
100105

106+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
107+
$iriConverterProphecy->getIriFromItem($dummy)->willReturn('/dummy/1')->shouldBeCalled();
108+
101109
$dataPersisterProphecy
102110
->persist($dummy)
103111
->willReturn($dummy2) // Persist is not mutating $dummy, but return a brand new technically unrelated object instead
@@ -117,9 +125,10 @@ public function testOnKernelViewWithControllerResultAndPersistWithImmutableResou
117125

118126
$request->setMethod($httpMethod);
119127

120-
(new WriteListener($dataPersisterProphecy->reveal()))->onKernelView($event);
128+
(new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event);
121129

122130
$this->assertSame($dummy2, $event->getControllerResult());
131+
$this->assertEquals('/dummy/1', $request->attributes->get('_api_write_item_iri'));
123132
}
124133
}
125134

@@ -132,6 +141,9 @@ public function testOnKernelViewWithControllerResultAndRemove()
132141
$dataPersisterProphecy->supports($dummy)->willReturn(true)->shouldBeCalled();
133142
$dataPersisterProphecy->remove($dummy)->shouldBeCalled();
134143

144+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
145+
$iriConverterProphecy->getIriFromItem($dummy)->shouldNotBeCalled();
146+
135147
$request = new Request();
136148
$request->setMethod('DELETE');
137149
$request->attributes->set('_api_resource_class', Dummy::class);
@@ -143,7 +155,7 @@ public function testOnKernelViewWithControllerResultAndRemove()
143155
$dummy
144156
);
145157

146-
(new WriteListener($dataPersisterProphecy->reveal()))->onKernelView($event);
158+
(new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event);
147159
}
148160

149161
public function testOnKernelViewWithSafeMethod()
@@ -156,6 +168,9 @@ public function testOnKernelViewWithSafeMethod()
156168
$dataPersisterProphecy->persist($dummy)->shouldNotBeCalled();
157169
$dataPersisterProphecy->remove($dummy)->shouldNotBeCalled();
158170

171+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
172+
$iriConverterProphecy->getIriFromItem($dummy)->shouldNotBeCalled();
173+
159174
$request = new Request();
160175
$request->setMethod('HEAD');
161176
$request->attributes->set('_api_resource_class', Dummy::class);
@@ -180,6 +195,9 @@ public function testOnKernelViewWithPersistFlagOff()
180195
$dataPersisterProphecy->persist($dummy)->shouldNotBeCalled();
181196
$dataPersisterProphecy->remove($dummy)->shouldNotBeCalled();
182197

198+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
199+
$iriConverterProphecy->getIriFromItem($dummy)->shouldNotBeCalled();
200+
183201
$request = new Request();
184202
$request->setMethod('HEAD');
185203
$request->attributes->set('_api_resource_class', Dummy::class);
@@ -192,7 +210,7 @@ public function testOnKernelViewWithPersistFlagOff()
192210
$dummy
193211
);
194212

195-
(new WriteListener($dataPersisterProphecy->reveal()))->onKernelView($event);
213+
(new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event);
196214
}
197215

198216
public function testOnKernelViewWithNoResourceClass()
@@ -205,6 +223,9 @@ public function testOnKernelViewWithNoResourceClass()
205223
$dataPersisterProphecy->persist($dummy)->shouldNotBeCalled();
206224
$dataPersisterProphecy->remove($dummy)->shouldNotBeCalled();
207225

226+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
227+
$iriConverterProphecy->getIriFromItem($dummy)->shouldNotBeCalled();
228+
208229
$request = new Request();
209230
$request->setMethod('POST');
210231

@@ -215,7 +236,7 @@ public function testOnKernelViewWithNoResourceClass()
215236
$dummy
216237
);
217238

218-
(new WriteListener($dataPersisterProphecy->reveal()))->onKernelView($event);
239+
(new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event);
219240
}
220241

221242
public function testOnKernelViewWithNoDataPersisterSupport()
@@ -228,6 +249,9 @@ public function testOnKernelViewWithNoDataPersisterSupport()
228249
$dataPersisterProphecy->persist($dummy)->shouldNotBeCalled();
229250
$dataPersisterProphecy->remove($dummy)->shouldNotBeCalled();
230251

252+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
253+
$iriConverterProphecy->getIriFromItem($dummy)->shouldNotBeCalled();
254+
231255
$request = new Request();
232256
$request->setMethod('POST');
233257
$request->attributes->set('_api_resource_class', 'Dummy');
@@ -239,6 +263,6 @@ public function testOnKernelViewWithNoDataPersisterSupport()
239263
$dummy
240264
);
241265

242-
(new WriteListener($dataPersisterProphecy->reveal()))->onKernelView($event);
266+
(new WriteListener($dataPersisterProphecy->reveal(), $iriConverterProphecy->reveal()))->onKernelView($event);
243267
}
244268
}

0 commit comments

Comments
 (0)