Skip to content

Commit 196cd67

Browse files
committed
Fix CORS preflight handling
1 parent ece211e commit 196cd67

10 files changed

Lines changed: 114 additions & 24 deletions

File tree

src/nodenorm/handlers/base.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Shared handler behavior for NodeNormalization API endpoints."""
2+
3+
from biothings.web.handlers import BaseHandler
4+
5+
6+
class NodeNormalizationBaseHandler(BaseHandler):
7+
"""Base handler that keeps the lightweight BioThings handler plus CORS."""
8+
9+
cors_methods = "GET, POST, HEAD, OPTIONS"
10+
cors_max_age = "600"
11+
12+
def set_default_headers(self):
13+
origin = self.request.headers.get("Origin")
14+
if origin is None:
15+
return
16+
17+
requested_headers = self.request.headers.get("Access-Control-Request-Headers")
18+
19+
self.set_header("Access-Control-Allow-Origin", origin)
20+
self.set_header("Access-Control-Allow-Credentials", "true")
21+
self.set_header("Access-Control-Allow-Methods", self.cors_methods)
22+
self.set_header("Access-Control-Allow-Headers", requested_headers or "*")
23+
self.set_header("Access-Control-Max-Age", self.cors_max_age)
24+
self.set_header("Vary", "Origin")
25+
26+
def options(self, *args, **kwargs):
27+
self.finish()

src/nodenorm/handlers/conflations.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import logging
22

3-
from biothings.web.handlers import BaseHandler
4-
3+
from nodenorm.handlers.base import NodeNormalizationBaseHandler
54

65
logger = logging.getLogger(__name__)
76

87

9-
class ValidConflationsHandler(BaseHandler):
8+
class ValidConflationsHandler(NodeNormalizationBaseHandler):
109
name = "allowed-conflations"
1110

1211
async def get(self):

src/nodenorm/handlers/curie_prefix.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from biothings.web.handlers import BaseHandler
21
from tornado.web import HTTPError
32

43
from nodenorm.biolink import toolkit
4+
from nodenorm.handlers.base import NodeNormalizationBaseHandler
55

66

7-
class SemanticTypeHandler(BaseHandler):
7+
class SemanticTypeHandler(NodeNormalizationBaseHandler):
88
"""
99
Mirror implementation to the renci implementation found at
1010
https://nodenormalization-sri.renci.org/docs

src/nodenorm/handlers/health.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
from elasticsearch import AsyncElasticsearch
44

5-
from biothings.web.handlers import BaseHandler
6-
75
from nodenorm.biolink import BIOLINK_MODEL_VERSION
6+
from nodenorm.handlers.base import NodeNormalizationBaseHandler
87

98

10-
class NodeNormHealthHandler(BaseHandler):
9+
class NodeNormHealthHandler(NodeNormalizationBaseHandler):
1110
"""
1211
Important Endpoints
1312
* /_cat/nodes

src/nodenorm/handlers/normalized_nodes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
import time
55
from typing import Union
66

7-
from biothings.web.handlers import BaseHandler
87
from tornado.web import HTTPError
98

109
from nodenorm.biolink import toolkit
10+
from nodenorm.handlers.base import NodeNormalizationBaseHandler
1111
from nodenorm.namespace import NodeNormalizationAPINamespace
1212

1313
logger = logging.getLogger(__name__)
@@ -25,7 +25,7 @@ class NormalizedNode:
2525
taxa: list[str]
2626

2727

28-
class NormalizedNodesHandler(BaseHandler):
28+
class NormalizedNodesHandler(NodeNormalizationBaseHandler):
2929
"""
3030
Mirror implementation to the renci implementation found at
3131
https://nodenormalization-sri.renci.org/docs

src/nodenorm/handlers/semantic_types.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from biothings.web.handlers import BaseHandler
21
from tornado.web import HTTPError
32

43
from nodenorm.biolink import toolkit
4+
from nodenorm.handlers.base import NodeNormalizationBaseHandler
55

66

7-
class SemanticTypeHandler(BaseHandler):
7+
class SemanticTypeHandler(NodeNormalizationBaseHandler):
88
"""
99
Mirror implementation to the renci implementation found at
1010
https://nodenormalization-sri.renci.org/docs

src/nodenorm/handlers/set_identifiers.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
import uuid
77
from typing import Optional
88

9-
from biothings.web.handlers import BaseHandler
10-
119
from tornado.web import HTTPError
1210

11+
from nodenorm.handlers.base import NodeNormalizationBaseHandler
1312
from nodenorm.handlers.normalized_nodes import get_normalized_nodes
1413
from nodenorm.namespace import NodeNormalizationAPINamespace
1514

@@ -24,7 +23,7 @@ class SetIDResponse:
2423
setid: Optional[str] = None
2524

2625

27-
class SetIdentifierHandler(BaseHandler):
26+
class SetIdentifierHandler(NodeNormalizationBaseHandler):
2827
"""
2928
Mirror implementation to the renci implementation found at
3029
https://nodenormalization-sri.renci.org/docs

src/nodenorm/handlers/version.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
from biothings.web.handlers import BaseHandler
2-
1+
from nodenorm.handlers.base import NodeNormalizationBaseHandler
32
from nodenorm.version import get_version
43

54

6-
class VersionHandler(BaseHandler):
5+
class VersionHandler(NodeNormalizationBaseHandler):
76
name = "version"
87

98
async def get(self, *args, **kwargs):

src/nodenorm/namespace.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,9 @@ def load_configuration(self, option_configuration: tornado.options.OptionParser)
140140

141141
configuration.update(default_configuration)
142142

143-
if option_configuration.conf is not None:
144-
optional_configuration = pathlib.Path(option_configuration.conf).absolute().resolve()
143+
optional_configuration_file = getattr(option_configuration, "conf", None)
144+
if optional_configuration_file is not None:
145+
optional_configuration = pathlib.Path(optional_configuration_file).absolute().resolve()
145146
if optional_configuration.exists():
146147
with open(optional_configuration, "r", encoding="utf-8") as handle:
147148
configuration.update(json.load(handle))
@@ -155,11 +156,13 @@ def load_configuration(self, option_configuration: tornado.options.OptionParser)
155156
configuration_namespace = types.SimpleNamespace(**configuration)
156157

157158
# override options
158-
if option_configuration.host is not None:
159-
configuration_namespace.webserver["HOST"] = option_configuration.host
159+
option_host = getattr(option_configuration, "host", None)
160+
if option_host is not None:
161+
configuration_namespace.webserver["HOST"] = option_host
160162

161-
if option_configuration.port is not None:
162-
configuration_namespace.webserver["PORT"] = option_configuration.port
163+
option_port = getattr(option_configuration, "port", None)
164+
if option_port is not None:
165+
configuration_namespace.webserver["PORT"] = option_port
163166

164167
return configuration_namespace
165168

tests/test_cors.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import importlib.util
2+
from pathlib import Path
3+
4+
import tornado.web
5+
from tornado.testing import AsyncHTTPTestCase
6+
7+
8+
ORIGIN = "https://translatorsri.github.io"
9+
BASE_HANDLER_PATH = Path(__file__).parents[1] / "src" / "nodenorm" / "handlers" / "base.py"
10+
11+
12+
def load_base_handler():
13+
spec = importlib.util.spec_from_file_location("_nodenorm_base_handler_under_test", BASE_HANDLER_PATH)
14+
module = importlib.util.module_from_spec(spec)
15+
spec.loader.exec_module(module)
16+
return module.NodeNormalizationBaseHandler
17+
18+
19+
NodeNormalizationBaseHandler = load_base_handler()
20+
21+
22+
class PreflightHandler(NodeNormalizationBaseHandler):
23+
async def get(self):
24+
self.finish({"ok": True})
25+
26+
27+
def assert_cors_headers(headers, allowed_headers="*"):
28+
assert headers["Access-Control-Allow-Origin"] == ORIGIN
29+
assert headers["Access-Control-Allow-Credentials"] == "true"
30+
assert headers["Access-Control-Allow-Methods"] == "GET, POST, HEAD, OPTIONS"
31+
assert headers["Access-Control-Allow-Headers"] == allowed_headers
32+
assert headers["Access-Control-Max-Age"] == "600"
33+
assert headers["Vary"] == "Origin"
34+
35+
36+
class TestCorsHeaders(AsyncHTTPTestCase):
37+
def get_app(self) -> tornado.web.Application:
38+
return tornado.web.Application(
39+
[
40+
(r"/version", PreflightHandler),
41+
(r"/get_normalized_nodes", PreflightHandler),
42+
]
43+
)
44+
45+
def test_get_response_includes_cors_headers_for_browser_origin(self):
46+
response = self.fetch("/version", headers={"Origin": ORIGIN})
47+
48+
assert response.code == 200
49+
assert_cors_headers(response.headers)
50+
51+
def test_preflight_returns_cors_headers(self):
52+
response = self.fetch(
53+
"/get_normalized_nodes",
54+
method="OPTIONS",
55+
headers={
56+
"Origin": ORIGIN,
57+
"Access-Control-Request-Method": "POST",
58+
"Access-Control-Request-Headers": "content-type",
59+
},
60+
)
61+
62+
assert response.code == 200
63+
assert response.body == b""
64+
assert_cors_headers(response.headers, "content-type")

0 commit comments

Comments
 (0)