[now-python] Fix headers with multiple values (#3053)

* [now-python] Add format_headers()

* Add tests

* Fix filenames

* Fix test probes
This commit is contained in:
Steven
2019-09-20 17:55:39 -04:00
parent e0b3e9606a
commit 8253e76ec0
8 changed files with 107 additions and 27 deletions

View File

@@ -7,21 +7,35 @@ import inspect
import __NOW_HANDLER_FILENAME import __NOW_HANDLER_FILENAME
__now_variables = dir(__NOW_HANDLER_FILENAME) __now_variables = dir(__NOW_HANDLER_FILENAME)
def format_headers(headers, decode=False):
keyToList = {}
for key, value in headers.items():
if decode:
key = key.decode()
value = value.decode()
if key not in keyToList:
keyToList[key] = []
keyToList[key].append(value)
return keyToList
if 'handler' in __now_variables or 'Handler' in __now_variables: if 'handler' in __now_variables or 'Handler' in __now_variables:
base = __NOW_HANDLER_FILENAME.handler if ('handler' in __now_variables) else __NOW_HANDLER_FILENAME.Handler base = __NOW_HANDLER_FILENAME.handler if ('handler' in __now_variables) else __NOW_HANDLER_FILENAME.Handler
if not issubclass(base, BaseHTTPRequestHandler): if not issubclass(base, BaseHTTPRequestHandler):
print('Handler must inherit from BaseHTTPRequestHandler') print('Handler must inherit from BaseHTTPRequestHandler')
print('See the docs https://zeit.co/docs/v2/deployments/official-builders/python-now-python') print('See the docs https://zeit.co/docs/v2/deployments/official-builders/python-now-python')
exit(1) exit(1)
print('using HTTP Handler') print('using HTTP Handler')
from http.server import HTTPServer from http.server import HTTPServer
from urllib.parse import unquote from urllib.parse import unquote
import requests import http
import _thread import _thread
server = HTTPServer(('', 0), base) server = HTTPServer(('', 0), base)
port = server.server_address[1] port = server.server_address[1]
def now_handler(event, context): def now_handler(event, context):
_thread.start_new_thread(server.handle_request, ()) _thread.start_new_thread(server.handle_request, ())
@@ -38,13 +52,15 @@ if 'handler' in __now_variables or 'Handler' in __now_variables:
): ):
body = base64.b64decode(body) body = base64.b64decode(body)
res = requests.request(method, 'http://0.0.0.0:' + str(port) + path, conn = http.client.HTTPConnection('0.0.0.0', port)
headers=headers, data=body, allow_redirects=False) conn.request(method, path, headers=headers, body=body)
res = conn.getresponse()
data = res.read().decode('utf-8')
return { return {
'statusCode': res.status_code, 'statusCode': res.status,
'headers': dict(res.headers), 'headers': format_headers(res.headers),
'body': res.text, 'body': data,
} }
elif 'app' in __now_variables: elif 'app' in __now_variables:
if ( if (
@@ -60,6 +76,7 @@ elif 'app' in __now_variables:
from werkzeug._compat import wsgi_encoding_dance from werkzeug._compat import wsgi_encoding_dance
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from werkzeug.wrappers import Response from werkzeug.wrappers import Response
def now_handler(event, context): def now_handler(event, context):
payload = json.loads(event['body']) payload = json.loads(event['body'])
@@ -113,7 +130,7 @@ elif 'app' in __now_variables:
return_dict = { return_dict = {
'statusCode': response.status_code, 'statusCode': response.status_code,
'headers': dict(response.headers) 'headers': format_headers(response.headers)
} }
if response.data: if response.data:
@@ -126,6 +143,7 @@ elif 'app' in __now_variables:
import asyncio import asyncio
import enum import enum
from urllib.parse import urlparse, unquote, urlencode from urllib.parse import urlparse, unquote, urlencode
from werkzeug.datastructures import Headers
class ASGICycleState(enum.Enum): class ASGICycleState(enum.Enum):
@@ -180,7 +198,7 @@ elif 'app' in __now_variables:
) )
status_code = message['status'] status_code = message['status']
headers = {k: v for k, v in message.get('headers', [])} headers = Headers(message.get('headers', []))
self.on_request(headers, status_code) self.on_request(headers, status_code)
self.state = ASGICycleState.RESPONSE self.state = ASGICycleState.RESPONSE
@@ -203,7 +221,7 @@ elif 'app' in __now_variables:
def on_request(self, headers, status_code): def on_request(self, headers, status_code):
self.response['statusCode'] = status_code self.response['statusCode'] = status_code
self.response['headers'] = {k.decode(): v.decode() for k, v in headers.items()} self.response['headers'] = format_headers(headers, decode=True)
def on_response(self): def on_response(self):
if self.body: if self.body:
@@ -250,4 +268,3 @@ else:
print('Missing variable `handler` or `app` in file __NOW_HANDLER_FILENAME.py') print('Missing variable `handler` or `app` in file __NOW_HANDLER_FILENAME.py')
print('See the docs https://zeit.co/docs/v2/deployments/official-builders/python-now-python') print('See the docs https://zeit.co/docs/v2/deployments/official-builders/python-now-python')
exit(1) exit(1)

View File

@@ -106,7 +106,6 @@ export const build = async ({
console.log('Installing dependencies...'); console.log('Installing dependencies...');
await pipInstall(pipPath, workPath, 'werkzeug'); await pipInstall(pipPath, workPath, 'werkzeug');
await pipInstall(pipPath, workPath, 'requests');
let fsFiles = await glob('**', workPath); let fsFiles = await glob('**', workPath);
const entryDirectory = dirname(entrypoint); const entryDirectory = dirname(entrypoint);

View File

@@ -0,0 +1,19 @@
async def app(scope, receive, send):
assert scope["type"] == "http"
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [
[b"content-type", b"text/plain"],
[b"set-cookie", b"one=first"],
[b"set-cookie", b"two=second"]
]
}
)
await send(
{
"type": "http.response.body",
"body": b"asgi:RANDOMNESS_PLACEHOLDER"
}
)

View File

@@ -0,0 +1,12 @@
from http.server import BaseHTTPRequestHandler
class handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('content-type', 'text/plain')
self.send_header('set-cookie', 'one=first')
self.send_header('set-cookie', 'two=second')
self.end_headers()
self.wfile.write('handler:RANDOMNESS_PLACEHOLDER'.encode())
return

View File

@@ -0,0 +1,11 @@
from flask import Flask, Response
app = Flask(__name__)
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
r = Response('wsgi:RANDOMNESS_PLACEHOLDER', mimetype='text/plain')
r.set_cookie('one', 'first')
r.set_cookie('two', 'second')
return r

View File

@@ -0,0 +1,24 @@
{
"version": 2,
"builds": [{ "src": "**/**.py", "use": "@now/python" }],
"probes": [
{
"path": "/cookie_asgi.py",
"responseHeaders": {
"set-cookie": ["one=first", "two=second"]
}
},
{
"path": "/cookie_handler.py",
"responseHeaders": {
"set-cookie": ["one=first", "two=second"]
}
},
{
"path": "/cookie_wsgi.py",
"responseHeaders": {
"set-cookie": ["one=first", "two=second"]
}
}
]
}

View File

@@ -0,0 +1 @@
Flask==1.0.2

View File

@@ -93,9 +93,7 @@ async function testDeployment (
if (probe.status) { if (probe.status) {
if (probe.status !== resp.status) { if (probe.status !== resp.status) {
throw new Error( throw new Error(
`Fetched page ${probeUrl} does not return the status ${ `Fetched page ${probeUrl} does not return the status ${probe.status} Instead it has ${resp.status}`
probe.status
} Instead it has ${resp.status}`
); );
} }
} }
@@ -107,23 +105,22 @@ async function testDeployment (
.map(([ k, v ]) => ` ${k}=${v}`) .map(([ k, v ]) => ` ${k}=${v}`)
.join('\n'); .join('\n');
throw new Error( throw new Error(
`Fetched page ${probeUrl} does not contain ${probe.mustContain}.` `Fetched page ${probeUrl} does not contain ${probe.mustContain}.` +
+ ` Instead it contains ${text.slice(0, 60)}` ` Instead it contains ${text.slice(0, 60)}` +
+ ` Response headers:\n ${headers}` ` Response headers:\n ${headers}`
); );
} }
} else if (probe.responseHeaders) { } else if (probe.responseHeaders) {
// eslint-disable-next-line no-loop-func // eslint-disable-next-line no-loop-func
Object.keys(probe.responseHeaders).forEach((header) => { Object.keys(probe.responseHeaders).forEach((header) => {
if (resp.headers.get(header) !== probe.responseHeaders[header]) { const actual = resp.headers.get(header);
const headers = Array.from(resp.headers.entries()) const expected = probe.responseHeaders[header];
.map(([ k, v ]) => ` ${k}=${v}`) const isEqual = Array.isArray(expected)
.join('\n'); ? expected.every((h) => actual.includes(h))
: expected === actual;
if (!isEqual) {
throw new Error( throw new Error(
`Fetched page ${probeUrl} does not contain header ${header}: \`${ `Page ${probeUrl} does not have header ${header}.\n\nExpected: ${expected}.\nActual: ${headers}`
probe.responseHeaders[header]
}\`.\n\nResponse headers:\n ${headers}`
); );
} }
}); });