diff --git a/httpbin/core.py b/httpbin/core.py
index e23cc86b..6cfeb0a1 100644
--- a/httpbin/core.py
+++ b/httpbin/core.py
@@ -33,7 +33,10 @@
except ImportError: # werkzeug < 2.1
from werkzeug.wrappers import BaseResponse as Response
-from flasgger import Swagger, NO_SANITIZER
+try:
+ from flasgger import Swagger, NO_SANITIZER
+except ImportError:
+ Swagger = False
from . import filters
from .helpers import (
@@ -95,77 +98,83 @@ def jsonify(*args, **kwargs):
app.config["SWAGGER"] = {"title": "httpbin.org", "uiversion": 3}
-template = {
- "swagger": "2.0",
- "info": {
- "title": "httpbin.org",
- "description": (
- "A simple HTTP Request & Response Service."
- "
A Kenneth Reitz project."
- "
Run locally:
"
- "$ docker pull ghcr.io/psf/httpbin
"
- "$ docker run -p 80:8080 ghcr.io/psf/httpbin"
- ),
- "contact": {
- "responsibleOrganization": "Python Software Foundation",
- "responsibleDeveloper": "Kenneth Reitz",
- "url": "https://github.com/psf/httpbin/",
- },
- # "termsOfService": "http://me.com/terms",
- "version": version,
- },
- "host": "httpbin.org", # overrides localhost:5000
- "basePath": "/", # base bash for blueprint registration
- "schemes": ["https"],
- "protocol": "https",
- "tags": [
- {
- "name": "HTTP Methods",
- "description": "Testing different HTTP verbs",
- # 'externalDocs': {'description': 'Learn more', 'url': 'https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html'}
- },
- {"name": "Auth", "description": "Auth methods"},
- {
- "name": "Status codes",
- "description": "Generates responses with given status code",
- },
- {"name": "Request inspection", "description": "Inspect the request data"},
- {
- "name": "Response inspection",
- "description": "Inspect the response data like caching and headers",
- },
- {
- "name": "Response formats",
- "description": "Returns responses in different data formats",
- },
- {"name": "Dynamic data", "description": "Generates random and dynamic data"},
- {"name": "Cookies", "description": "Creates, reads and deletes Cookies"},
- {"name": "Images", "description": "Returns different image formats"},
- {"name": "Redirects", "description": "Returns different redirect responses"},
- {
- "name": "Anything",
- "description": "Returns anything that is passed to request",
+if Swagger:
+ template = {
+ "swagger": "2.0",
+ "info": {
+ "title": "httpbin.org",
+ "description": (
+ "A simple HTTP Request & Response Service."
+ "
A Kenneth Reitz project."
+ "
Run locally:
"
+ "$ docker pull ghcr.io/psf/httpbin
"
+ "$ docker run -p 80:8080 ghcr.io/psf/httpbin"
+ ),
+ "contact": {
+ "responsibleOrganization": "Python Software Foundation",
+ "responsibleDeveloper": "Kenneth Reitz",
+ "url": "https://github.com/psf/httpbin/",
+ },
+ # "termsOfService": "http://me.com/terms",
+ "version": version,
},
- ],
-}
-
-swagger_config = {
- "headers": [],
- "specs": [
- {
- "endpoint": "spec",
- "route": "/spec.json",
- "rule_filter": lambda rule: True, # all in
- "model_filter": lambda tag: True, # all in
- }
- ],
- "static_url_path": "/flasgger_static",
- # "static_folder": "static", # must be set by user
- "swagger_ui": True,
- "specs_route": "/",
-}
+ "host": "httpbin.org", # overrides localhost:5000
+ "basePath": "/", # base bash for blueprint registration
+ "schemes": ["https"],
+ "protocol": "https",
+ "tags": [
+ {
+ "name": "HTTP Methods",
+ "description": "Testing different HTTP verbs",
+ # 'externalDocs': {'description': 'Learn more', 'url': 'https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html'}
+ },
+ {"name": "Auth", "description": "Auth methods"},
+ {
+ "name": "Status codes",
+ "description": "Generates responses with given status code",
+ },
+ {"name": "Request inspection", "description": "Inspect the request data"},
+ {
+ "name": "Response inspection",
+ "description": "Inspect the response data like caching and headers",
+ },
+ {
+ "name": "Response formats",
+ "description": "Returns responses in different data formats",
+ },
+ {"name": "Dynamic data", "description": "Generates random and dynamic data"},
+ {"name": "Cookies", "description": "Creates, reads and deletes Cookies"},
+ {"name": "Images", "description": "Returns different image formats"},
+ {"name": "Redirects", "description": "Returns different redirect responses"},
+ {
+ "name": "Anything",
+ "description": "Returns anything that is passed to request",
+ },
+ ],
+ }
+
+ swagger_config = {
+ "headers": [],
+ "specs": [
+ {
+ "endpoint": "spec",
+ "route": "/spec.json",
+ "rule_filter": lambda rule: True, # all in
+ "model_filter": lambda tag: True, # all in
+ }
+ ],
+ "static_url_path": "/flasgger_static",
+ # "static_folder": "static", # must be set by user
+ "swagger_ui": True,
+ "specs_route": "/",
+ }
-swagger = Swagger(app, sanitizer=NO_SANITIZER, template=template, config=swagger_config)
+ swagger = Swagger(app, sanitizer=NO_SANITIZER, template=template, config=swagger_config)
+else:
+ app.logger.warning(
+ "flasgger is not installed; serving the static landing page at / "
+ "and skipping the Swagger UI and /spec.json."
+ )
# Set up Bugsnag exception tracking, if desired. To use Bugsnag, install the
# Bugsnag Python client with the command "pip install bugsnag", and set the
@@ -243,8 +252,12 @@ def set_cors_headers(response):
# Routes
# ------
+if Swagger:
+ staticroute = "/legacy"
+else:
+ staticroute = "/"
-@app.route("/legacy")
+@app.route(staticroute)
def view_landing_page():
"""Generates Landing Page in legacy layout."""
return render_template("index.html")
diff --git a/pyproject.toml b/pyproject.toml
index 6c7a58ca..d5cc9f0e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -34,7 +34,6 @@ classifiers = [
dependencies = [
"brotlicffi",
"decorator",
- "flasgger",
"flask >= 2.2.4",
'greenlet < 3.0; python_version<"3.12"',
'greenlet >= 3.0.0a1; python_version>="3.12.0rc0"',
@@ -46,6 +45,7 @@ dependencies = [
[project.optional-dependencies]
test = ["pytest", "tox"]
mainapp = [
+ "flasgger",
"gunicorn",
"gevent",
]
diff --git a/tests/test_httpbin.py b/tests/test_httpbin.py
index 9d995b93..eb1d315c 100755
--- a/tests/test_httpbin.py
+++ b/tests/test_httpbin.py
@@ -1,7 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
+import sys
import base64
+import subprocess
import unittest
import contextlib
import json
@@ -796,6 +798,31 @@ def test_etag_with_no_headers(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers.get('ETag'), 'abc')
+ def test_index_falls_back_to_static_page_without_flasgger(self):
+ """When flasgger isn't installed, / serves the static legacy landing
+ page (HTTP 200) instead of the Swagger UI, rather than 500ing.
+
+ flasgger is imported at module load, so this runs in a subprocess that
+ poisons sys.modules['flasgger'] before httpbin is imported.
+ """
+ code = (
+ "import sys\n"
+ "sys.modules['flasgger'] = None\n" # makes `import flasgger` raise ImportError
+ "import httpbin\n"
+ "assert httpbin.core.Swagger is False, repr(httpbin.core.Swagger)\n"
+ "r = httpbin.app.test_client().get('/')\n"
+ "assert r.status_code == 200, r.status_code\n"
+ "body = r.get_data(as_text=True)\n"
+ "assert 'httpbin(1): HTTP Client Testing Service' in body, 'legacy page not served'\n"
+ "assert 'swagger-ui' not in body, 'swagger UI unexpectedly rendered'\n"
+ "print('OK')\n"
+ )
+ result = subprocess.run(
+ [sys.executable, "-c", code], capture_output=True, text=True
+ )
+ self.assertEqual(result.returncode, 0, result.stderr)
+ self.assertIn("OK", result.stdout)
+
def test_parse_multi_value_header(self):
self.assertEqual(parse_multi_value_header('xyzzy'), [ "xyzzy" ])
self.assertEqual(parse_multi_value_header('"xyzzy"'), [ "xyzzy" ])