Skip to content

Commit 906e05a

Browse files
dsotirho-ucschannes-ucsc
authored andcommitted
[1/2] [a] Fix: Can't use curl to download a single manifest in one invocation (#5918)
Add support for POST requests to the manifest endpoint
1 parent 02252d8 commit 906e05a

File tree

4 files changed

+1505
-48
lines changed

4 files changed

+1505
-48
lines changed

lambdas/service/app.py

Lines changed: 96 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,26 +1057,27 @@ def parameter_hoisting_note(method: str,
10571057
) -> str:
10581058
return fd('''
10591059
Any of the query parameters documented below can alternatively be passed
1060-
as a property of a JSON object in the body of the request. This can be
1061-
useful in case the value of the `filters` query parameter causes the URL
1062-
to exceed the maximum length of 8192 characters, resulting in a 413
1063-
Request Entity Too Large response.
1060+
as a property of a JSON object in the body of the request. This is
1061+
referred to as *parameter hoisting* and can be useful in case the value
1062+
of the `filters` query parameter causes the URL to exceed the maximum
1063+
length of 8192 characters, resulting in a 413 Request Entity Too Large
1064+
response.
10641065
10651066
The request `%s %s?filters={…}`, for example, is equivalent to `%s %s`
1066-
with the body `{"filters": "{…}"}` in which any double quotes or
1067-
backslash characters inside `…` are escaped with another backslash. That
1068-
escaping is the requisite procedure for embedding one JSON structure
1069-
inside another.
1067+
with a `Content-Type` header of `application/json` and the body
1068+
`{"filters": "{…}"}` in which any double quotes or backslash characters
1069+
inside `…` are escaped with another backslash. That escaping is the
1070+
requisite procedure for embedding one JSON structure inside another.
10701071
''' % (method, endpoint, equivalent_method, endpoint))
10711072

10721073

10731074
def repository_search_spec(*, post: bool):
10741075
id_spec_link = '#operations-Index-get_index__entity_type___entity_id_'
10751076
return {
1076-
'summary': fd(f'''
1077-
Search an index for entities of interest
1078-
{", with filters provided in the request body" if post else ""}.
1079-
'''),
1077+
'summary': (
1078+
'Search an index for entities of interest' +
1079+
iif(post, ', with large parameters provided in the request body')
1080+
),
10801081
'deprecated': post,
10811082
'description':
10821083
iif(post, parameter_hoisting_note('GET', '/index/files', 'POST') + fd('''
@@ -1312,14 +1313,34 @@ def get_summary():
13121313
authentication=request.authentication)
13131314

13141315

1315-
def manifest_route(*, fetch: bool, initiate: bool):
1316+
post_manifest_example_url = (
1317+
f'{app.base_url}/manifest/files'
1318+
f'?catalog={list(config.catalogs.keys())[0]}'
1319+
'&filters={…}'
1320+
f'&format={app.metadata_plugin.manifest_formats[0].value}'
1321+
)
1322+
1323+
1324+
def manifest_route(*, fetch: bool, initiate: bool, curl: bool = False):
1325+
if initiate:
1326+
if curl:
1327+
assert not fetch
1328+
method = 'POST'
1329+
else:
1330+
method = 'PUT'
1331+
else:
1332+
assert not curl
1333+
method = 'GET'
13161334
return app.route(
13171335
# The path parameter could be a token *or* an object key, but we don't
13181336
# want to complicate the API with this detail
13191337
('/fetch' if fetch else '')
13201338
+ ('/manifest/files' if initiate else '/manifest/files/{token}'),
13211339
# The initial PUT request is idempotent.
1322-
methods=['PUT' if initiate else 'GET'],
1340+
methods=[method],
1341+
# In order to support requests made with `curl` and its `--data` option,
1342+
# we accept the `application/x-www-form-urlencoded` content-type.
1343+
content_types=['application/json', 'application/x-www-form-urlencoded'],
13231344
interactive=fetch,
13241345
cors=True,
13251346
path_spec=None if initiate else {
@@ -1331,26 +1352,58 @@ def manifest_route(*, fetch: bool, initiate: bool):
13311352
},
13321353
method_spec={
13331354
'tags': ['Manifests'],
1355+
'deprecated': curl,
13341356
'summary':
13351357
(
13361358
'Initiate the preparation of a manifest'
13371359
if initiate else
13381360
'Determine status of a manifest preparation job'
13391361
) + (
13401362
' via XHR' if fetch else ''
1363+
) + (
1364+
' as an alternative to PUT for curl users' if curl else ''
13411365
),
1342-
'description': fd('''
1343-
Create a manifest preparation job, returning either
1344-
1345-
- a 301 redirect to the URL of the status of that job or
1366+
'description': (
1367+
fd('''
1368+
Create a manifest preparation job, returning either
13461369
1347-
- a 302 redirect to the URL of an already prepared manifest.
1370+
- a 301 redirect to the URL of the status of that job or
13481371
1349-
This endpoint is not suitable for interactive use via the
1350-
Swagger UI. Please use [PUT /fetch/manifest/files][1] instead.
1372+
- a 302 redirect to the URL of an already prepared manifest.
1373+
''')
1374+
+ iif(not curl, fd(f'''
1375+
This endpoint is not suitable for interactive use via the
1376+
Swagger UI. Please use [{method} /fetch/manifest/files][1]
1377+
instead.
13511378
1352-
[1]: #operations-Manifests-put_fetch_manifest_files
1353-
''') + parameter_hoisting_note('PUT', '/manifest/files', 'PUT')
1379+
[1]: #operations-Manifests-{method.lower()}_fetch_manifest_files
1380+
'''))
1381+
+ parameter_hoisting_note(method, '/manifest/files', method)
1382+
+ iif(curl, fd(f'''
1383+
Requests to this endpoint are idempotent, so PUT would be
1384+
the more standards-compliant method to use. POST is offered
1385+
as a convenience for `curl` users, exploiting the fact that
1386+
`curl` drops to GET when following a redirect in response to
1387+
a POST, but not a PUT request. This is the only reason for
1388+
the deprecation of this endpoint and there are currently no
1389+
plans to remove it.
1390+
1391+
To use this endpoint with `curl`, pass the `--location` and
1392+
`--data` options. This makes `curl` automatically follow the
1393+
intermediate redirects to the GET /manifest/files endpoint,
1394+
and ultimately to the URL that yields the manifest. Example:
1395+
1396+
```
1397+
curl --data "" --location {post_manifest_example_url}
1398+
```
1399+
1400+
In order to facilitate this, a POST request to this endpoint
1401+
may have a `Content-Type` header of
1402+
`application/x-www-form-urlencoded`, which is what the
1403+
`--data` option sends. The body must be empty in that case
1404+
and parameters cannot be hoisted as described above.
1405+
'''))
1406+
)
13541407
if initiate and not fetch else
13551408
fd('''
13561409
Check on the status of an ongoing manifest preparation job,
@@ -1366,15 +1419,17 @@ def manifest_route(*, fetch: bool, initiate: bool):
13661419
instead.
13671420
13681421
[1]: #operations-Manifests-get_fetch_manifest_files__token_
1369-
''') if not initiate and not fetch else fd('''
1422+
''')
1423+
if not initiate and not fetch else
1424+
fd(f'''
13701425
Create a manifest preparation job, returning a 200 status
13711426
response whose JSON body emulates the HTTP headers that would be
1372-
found in a response to an equivalent request to the [PUT
1427+
found in a response to an equivalent request to the [{method}
13731428
/manifest/files][1] endpoint.
13741429
13751430
Whenever client-side JavaScript code is used in a web
13761431
application to request the preparation of a manifest from Azul,
1377-
this endpoint should be used instead of [PUT
1432+
this endpoint should be used instead of [{method}
13781433
/manifest/files][1]. This way, the client can use XHR to make
13791434
the request, retaining full control over the handling of
13801435
redirects and enabling the client to bypass certain limitations
@@ -1384,8 +1439,9 @@ def manifest_route(*, fetch: bool, initiate: bool):
13841439
upper limit on the number of consecutive redirects, before the
13851440
manifest generation job is done.
13861441
1387-
[1]: #operations-Manifests-put_manifest_files
1388-
''') + parameter_hoisting_note('PUT', '/fetch/manifest/files', 'PUT')
1442+
[1]: #operations-Manifests-{method.lower()}_manifest_files
1443+
''')
1444+
+ parameter_hoisting_note(method, '/fetch/manifest/files', method)
13891445
if initiate and fetch else
13901446
fd('''
13911447
Check on the status of an ongoing manifest preparation job,
@@ -1530,10 +1586,10 @@ def manifest_route(*, fetch: bool, initiate: bool):
15301586
15311587
For a detailed description of these properties see the
15321588
documentation for the respective response headers
1533-
documented under ''') + (fd('''
1534-
[PUT /manifest/files][1].
1589+
documented under ''') + (fd(f'''
1590+
[{method} /manifest/files][1].
15351591
1536-
[1]: #operations-Manifests-put_manifest_files
1592+
[1]: #operations-Manifests-{method.lower()}_manifest_files
15371593
''') if initiate else fd('''
15381594
[GET /manifest/files/{token}][1].
15391595
@@ -1575,6 +1631,7 @@ def manifest_route(*, fetch: bool, initiate: bool):
15751631
)
15761632

15771633

1634+
@manifest_route(fetch=False, initiate=True, curl=True)
15781635
@manifest_route(fetch=False, initiate=True)
15791636
def file_manifest():
15801637
return _file_manifest(fetch=False)
@@ -1597,6 +1654,14 @@ def fetch_file_manifest_with_token(token: str):
15971654

15981655
def _file_manifest(fetch: bool, token_or_key: Optional[str] = None):
15991656
request = app.current_request
1657+
post = request.method == 'POST'
1658+
if (
1659+
post
1660+
and request.headers.get('content-type') == 'application/x-www-form-urlencoded'
1661+
and request.raw_body != b''
1662+
):
1663+
raise BRE('The body must be empty for a POST request of content-type '
1664+
'`application/x-www-form-urlencoded` to this endpoint')
16001665
query_params = request.query_params or {}
16011666
_hoist_parameters(query_params, request)
16021667
if token_or_key is None:

0 commit comments

Comments
 (0)