mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-09 12:57:46 +00:00
[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:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
19
packages/now-python/test/fixtures/20-multivalue-header/cookie_asgi.py
vendored
Normal file
19
packages/now-python/test/fixtures/20-multivalue-header/cookie_asgi.py
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
)
|
||||||
12
packages/now-python/test/fixtures/20-multivalue-header/cookie_handler.py
vendored
Normal file
12
packages/now-python/test/fixtures/20-multivalue-header/cookie_handler.py
vendored
Normal 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
|
||||||
11
packages/now-python/test/fixtures/20-multivalue-header/cookie_wsgi.py
vendored
Normal file
11
packages/now-python/test/fixtures/20-multivalue-header/cookie_wsgi.py
vendored
Normal 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
|
||||||
24
packages/now-python/test/fixtures/20-multivalue-header/now.json
vendored
Normal file
24
packages/now-python/test/fixtures/20-multivalue-header/now.json
vendored
Normal 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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1
packages/now-python/test/fixtures/20-multivalue-header/requirements.txt
vendored
Normal file
1
packages/now-python/test/fixtures/20-multivalue-header/requirements.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Flask==1.0.2
|
||||||
@@ -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}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user