Skip to content

flask to fastapi conversion#187

Closed
ryansurf wants to merge 1 commit into
mainfrom
feat-fastapi
Closed

flask to fastapi conversion#187
ryansurf wants to merge 1 commit into
mainfrom
feat-fastapi

Conversation

@ryansurf
Copy link
Copy Markdown
Owner

@ryansurf ryansurf commented Mar 24, 2026

General:

  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same update/change?

Code:

  1. Does your submission pass tests?
  2. Have you run the linter/formatter on your code locally before submission?
  3. Have you updated the documentation/README to reflect your changes, as applicable?
  4. Have you added an explanation of what your changes do?
  5. Have you written new tests for your changes, as applicable?

Summary by Sourcery

Migrate the HTTP server from Flask to FastAPI, updating dependencies, runtime wiring, and tests accordingly.

New Features:

  • Expose the existing CLI-based surf report and help endpoints via a FastAPI application.

Bug Fixes:

  • Return proper HTTP 500 responses from the root endpoint when the CLI subprocess fails instead of propagating raw errors.

Enhancements:

  • Replace Flask application factory and CORS setup with FastAPI and CORSMiddleware, including Uvicorn-based startup configuration.
  • Update environment variable handling in the HTML template to use a FastAPI-safe Jinja expression.
  • Adjust tests to use FastAPI's TestClient and text-based response accessors.

Build:

  • Raise the minimum supported Python version to 3.10 and relax the pydantic version constraint.
  • Add FastAPI, Uvicorn, and httpx as application dependencies.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Mar 24, 2026

Reviewer's Guide

Migrates the HTTP server from Flask to FastAPI/uvicorn, adjusts endpoints and tests accordingly, and updates dependencies and template usage to match the new stack.

Sequence diagram for FastAPI root endpoint invoking CLI subprocess

sequenceDiagram
    actor Browser
    participant Uvicorn
    participant FastAPIApp
    participant CLIProcess

    Browser->>Uvicorn: HTTP GET /
    Uvicorn->>FastAPIApp: dispatch request
    FastAPIApp->>FastAPIApp: parse query_params
    FastAPIApp->>FastAPIApp: build args string
    FastAPIApp->>CLIProcess: subprocess.run([python, cli.py, args])
    CLIProcess-->>FastAPIApp: stdout (surf report)
    FastAPIApp-->>Browser: 200 text/plain (surf report)

    note over FastAPIApp,CLIProcess: On subprocess error, FastAPIApp raises HTTPException(500)
Loading

Sequence diagram for FastAPI /help endpoint serving help.txt

sequenceDiagram
    actor Browser
    participant Uvicorn
    participant FastAPIApp
    participant FileSystem

    Browser->>Uvicorn: HTTP GET /help
    Uvicorn->>FastAPIApp: dispatch request
    FastAPIApp->>FileSystem: read BASE_DIR/help.txt
    FileSystem-->>FastAPIApp: file handle
    FastAPIApp-->>Browser: 200 FileResponse text/plain
Loading

File-Level Changes

Change Details Files
Replace Flask app with FastAPI application and adjust routing, CORS, CLI invocation, and error handling.
  • Instantiate FastAPI instead of Flask and configure CORSMiddleware for localhost origins with GET-only methods.
  • Serve help.txt via FastAPI FileResponse using a BASE_DIR-resolved path instead of Flask send_from_directory.
  • Change the root endpoint to an async FastAPI route that reads query parameters from Request.query_params, builds the CLI args, runs src/cli.py via subprocess, and returns stdout as plain text.
  • On subprocess failure, log stderr and raise a FastAPI HTTPException with 500 instead of re-raising the raw CalledProcessError.
  • Introduce BASE_DIR, TEMPLATES_DIR, and CLI_PATH constants and initialize env = ServerSettings() and app = create_app(env) at module import time.
  • Replace the Flask dev server startup with uvicorn.run using env.PORT for the port.
src/server.py
Update tests to use FastAPI TestClient and align assertions with FastAPI response interfaces.
  • Remove Flask-specific tests for /home and /script.js endpoints that no longer exist.
  • Use fastapi.testclient.TestClient instead of Flask’s test_client() helper when calling endpoints in tests.
  • Adjust response assertions to use resp.text instead of resp.data and preserve status code checks for success and error paths.
tests/test_server.py
tests/test_help_endpoint.py
tests/test_root.py
Update project dependencies for FastAPI stack and Python version, and adjust template usage for environment variables.
  • Bump Python requirement to ">=3.10" and relax pydantic version to ^2.12.5.
  • Add fastapi, uvicorn, and httpx as dependencies in pyproject.toml.
  • Keep Flask-related dependencies in place (Flask, flask-cors) even though server implementation moved to FastAPI.
  • Modify index.html to treat env_vars as already-serialized/safe JSON instead of using Flask’s tojson filter while keeping the script URL reference.
pyproject.toml
src/templates/index.html
poetry.lock

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue, 5 other issues, and left some high level feedback:

Security issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)

General comments:

  • The index.html template still assumes Flask’s url_for('serve_script') and a serve_script route, but those no longer exist; consider adding a FastAPI static file route (e.g., via StaticFiles) and updating the template to use request.url_for accordingly.
  • With the removal of the /home endpoint, there is no FastAPI route that renders index.html or passes env_vars into the template; if the frontend still relies on this, add a corresponding @app.get("/home") using templates.TemplateResponse and include both request and env_vars in the context.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `index.html` template still assumes Flask’s `url_for('serve_script')` and a `serve_script` route, but those no longer exist; consider adding a FastAPI static file route (e.g., via `StaticFiles`) and updating the template to use `request.url_for` accordingly.
- With the removal of the `/home` endpoint, there is no FastAPI route that renders `index.html` or passes `env_vars` into the template; if the frontend still relies on this, add a corresponding `@app.get("/home")` using `templates.TemplateResponse` and include both `request` and `env_vars` in the context.

## Individual Comments

### Comment 1
<location path="src/server.py" line_range="47-51" />
<code_context>

-    @app.route("/help")
-    def serve_help():
+    @app.get("/help")
+    async def serve_help():
         """Serves the help.txt file."""
-        return send_from_directory(
-            Path(__file__).resolve().parents[1], "help.txt"
-        )
-
-    @app.route("/home")
-    def serve_index():
-        """Serves index.html."""
-        return render_template("index.html", env_vars=env.model_dump())
-
-    @app.route("/script.js")
-    def serve_script():
-        """Serves the frontend JavaScript."""
-        return send_file("static/script.js")
-
-    @app.route("/")
-    def default_route():
+        HELP_FILE_PATH = BASE_DIR / "help.txt"
+        return FileResponse(path=HELP_FILE_PATH, media_type="text/plain")
+
+
</code_context>
<issue_to_address>
**suggestion:** Consider handling the case where `help.txt` is missing to avoid an uninformative 500 error.

If `help.txt` is missing or unreadable, this will likely return a generic 500. Consider checking `HELP_FILE_PATH.exists()` (and possibly readability) and raising a `HTTPException(status_code=404, detail="help.txt not found")` or similar to return a clearer response and avoid unnecessary tracebacks in the logs.

Suggested implementation:

```python
from pathlib import Path

from fastapi import HTTPException

```

```python
@app.get("/help")
async def serve_help():
    """Serves the help.txt file."""
    HELP_FILE_PATH = BASE_DIR / "help.txt"

    if not HELP_FILE_PATH.exists() or not HELP_FILE_PATH.is_file():
        # Avoid an uninformative 500 if the help file is missing
        raise HTTPException(status_code=404, detail="help.txt not found")

    return FileResponse(path=HELP_FILE_PATH, media_type="text/plain")

```
</issue_to_address>

### Comment 2
<location path="src/server.py" line_range="44-47" />
<code_context>
-        """Serves index.html."""
-        return render_template("index.html", env_vars=env.model_dump())
-
-    @app.route("/script.js")
-    def serve_script():
-        """Serves the frontend JavaScript."""
-        return send_file("static/script.js")
-
-    @app.route("/")
</code_context>
<issue_to_address>
**issue (bug_risk):** The template still references `url_for('serve_script')`, but the corresponding endpoint has been removed.

In `index.html` the script tag still uses `{{ url_for('serve_script') }}`, but the `serve_script` route was removed during the FastAPI migration. This will result in a broken script URL unless `serve_script` is defined elsewhere. Please either add an equivalent FastAPI endpoint (e.g., via `StaticFiles`) or update the template to reference the new script path.
</issue_to_address>

### Comment 3
<location path="src/templates/index.html" line_range="90" />
<code_context>
-      // load .env variables from Flask server
-      const env = {{ env_vars|tojson }};
+      // load .env variables from FastAPI server
+      const env = {{ env_vars | safe }};
     </script>
     <script src="{{ url_for('serve_script') }}"></script>
</code_context>
<issue_to_address>
**🚨 issue (security):** Using `| safe` for `env_vars` may introduce XSS risk and assumes it is already valid JS/JSON.

Previously `env_vars` was passed through `tojson`, which both produced valid JSON and escaped it for safe embedding. With `| safe`, the value is injected into the script as-is. If `env_vars` includes any user-controlled data, this is an XSS vector, and if it’s a Python dict rather than a JSON string, the JS will be invalid. Prefer a JSON-encoding approach compatible with your template engine instead of marking this value safe.
</issue_to_address>

### Comment 4
<location path="src/server.py" line_range="87" />
<code_context>
-    app = create_app(env)
-    app.run(host="0.0.0.0", port=env.PORT, debug=env.DEBUG)
+
+    uvicorn.run(app, host="0.0.0.0", port=env.PORT)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The `DEBUG` setting from `ServerSettings` is no longer used to control development features like reload.

With the switch to `uvicorn.run`, `env.DEBUG` no longer affects runtime behavior. If it’s still meant to control development mode, consider passing it through (e.g. `reload=env.DEBUG` and/or `log_level="debug" if env.DEBUG else "info"`) so behavior stays consistent.
</issue_to_address>

### Comment 5
<location path="tests/test_help_endpoint.py" line_range="14-18" />
<code_context>
     app = create_app(env)

-    client = app.test_client()
+    client = TestClient(app)
     resp = client.get("/help")

     assert resp.status_code == HTTPStatus.OK
-    assert len(resp.data) > 0
+    assert len(resp.text) > 0
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen the `/help` test by asserting the actual file contents and content type.

`len(resp.text) > 0` only checks that something is returned. With the new `FileResponse`, you can strengthen this by reading `help.txt` from disk and asserting equality with `resp.text`, and optionally asserting the content type header starts with `text/plain` to better guard against regressions in how `/help` is served.

Suggested implementation:

```python
from fastapi import FastAPI
from fastapi.testclient import TestClient
from http import HTTPStatus
from pathlib import Path

```

```python
    client = TestClient(app)
    resp = client.get("/help")

    assert resp.status_code == HTTPStatus.OK

    help_contents = Path("help.txt").read_text(encoding="utf-8")
    assert resp.text == help_contents
    assert resp.headers["content-type"].startswith("text/plain")

```
</issue_to_address>

### Comment 6
<location path="src/server.py" line_range="64-68" />
<code_context>
            result = subprocess.run(
                [sys.executable, str(CLI_PATH), args],
                capture_output=True,
                text=True,
                check=True,
            )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/server.py
Comment on lines +47 to +51
@app.get("/help")
async def serve_help():
"""Serves the help.txt file."""
return send_from_directory(
Path(__file__).resolve().parents[1], "help.txt"
)

@app.route("/home")
def serve_index():
"""Serves index.html."""
return render_template("index.html", env_vars=env.model_dump())

@app.route("/script.js")
def serve_script():
"""Serves the frontend JavaScript."""
return send_file("static/script.js")

@app.route("/")
def default_route():
HELP_FILE_PATH = BASE_DIR / "help.txt"
return FileResponse(path=HELP_FILE_PATH, media_type="text/plain")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider handling the case where help.txt is missing to avoid an uninformative 500 error.

If help.txt is missing or unreadable, this will likely return a generic 500. Consider checking HELP_FILE_PATH.exists() (and possibly readability) and raising a HTTPException(status_code=404, detail="help.txt not found") or similar to return a clearer response and avoid unnecessary tracebacks in the logs.

Suggested implementation:

from pathlib import Path

from fastapi import HTTPException
@app.get("/help")
async def serve_help():
    """Serves the help.txt file."""
    HELP_FILE_PATH = BASE_DIR / "help.txt"

    if not HELP_FILE_PATH.exists() or not HELP_FILE_PATH.is_file():
        # Avoid an uninformative 500 if the help file is missing
        raise HTTPException(status_code=404, detail="help.txt not found")

    return FileResponse(path=HELP_FILE_PATH, media_type="text/plain")

Comment thread src/server.py
Comment on lines -44 to -47
@app.route("/script.js")
def serve_script():
"""Serves the frontend JavaScript."""
return send_file("static/script.js")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The template still references url_for('serve_script'), but the corresponding endpoint has been removed.

In index.html the script tag still uses {{ url_for('serve_script') }}, but the serve_script route was removed during the FastAPI migration. This will result in a broken script URL unless serve_script is defined elsewhere. Please either add an equivalent FastAPI endpoint (e.g., via StaticFiles) or update the template to reference the new script path.

Comment thread src/templates/index.html
// load .env variables from Flask server
const env = {{ env_vars|tojson }};
// load .env variables from FastAPI server
const env = {{ env_vars | safe }};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Using | safe for env_vars may introduce XSS risk and assumes it is already valid JS/JSON.

Previously env_vars was passed through tojson, which both produced valid JSON and escaped it for safe embedding. With | safe, the value is injected into the script as-is. If env_vars includes any user-controlled data, this is an XSS vector, and if it’s a Python dict rather than a JSON string, the JS will be invalid. Prefer a JSON-encoding approach compatible with your template engine instead of marking this value safe.

Comment thread src/server.py
app = create_app(env)
app.run(host="0.0.0.0", port=env.PORT, debug=env.DEBUG)

uvicorn.run(app, host="0.0.0.0", port=env.PORT)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): The DEBUG setting from ServerSettings is no longer used to control development features like reload.

With the switch to uvicorn.run, env.DEBUG no longer affects runtime behavior. If it’s still meant to control development mode, consider passing it through (e.g. reload=env.DEBUG and/or log_level="debug" if env.DEBUG else "info") so behavior stays consistent.

Comment on lines +14 to +18
client = TestClient(app)
resp = client.get("/help")

assert resp.status_code == HTTPStatus.OK
assert len(resp.data) > 0
assert len(resp.text) > 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Strengthen the /help test by asserting the actual file contents and content type.

len(resp.text) > 0 only checks that something is returned. With the new FileResponse, you can strengthen this by reading help.txt from disk and asserting equality with resp.text, and optionally asserting the content type header starts with text/plain to better guard against regressions in how /help is served.

Suggested implementation:

from fastapi import FastAPI
from fastapi.testclient import TestClient
from http import HTTPStatus
from pathlib import Path
    client = TestClient(app)
    resp = client.get("/help")

    assert resp.status_code == HTTPStatus.OK

    help_contents = Path("help.txt").read_text(encoding="utf-8")
    assert resp.text == help_contents
    assert resp.headers["content-type"].startswith("text/plain")

Comment thread src/server.py
Comment on lines 64 to 68
result = subprocess.run(
[sys.executable, Path("src") / "cli.py", args],
[sys.executable, str(CLI_PATH), args],
capture_output=True,
text=True,
check=True,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/server.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ryansurf ryansurf closed this Mar 24, 2026
@ryansurf ryansurf deleted the feat-fastapi branch March 24, 2026 00:51
@ryansurf ryansurf restored the feat-fastapi branch March 24, 2026 00:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant