Skip to content

Fix KRX API 400 Bad Request - Add Session-Based Authentication#196

Merged
mgh3326 merged 5 commits intomainfrom
auto-claude/001-fix-github-issue-195-auto-trader
Mar 5, 2026
Merged

Fix KRX API 400 Bad Request - Add Session-Based Authentication#196
mgh3326 merged 5 commits intomainfrom
auto-claude/001-fix-github-issue-195-auto-trader

Conversation

@robin-watcha
Copy link
Collaborator

@robin-watcha robin-watcha commented Mar 5, 2026

The screen_stocks MCP tool fails when called with market=kr, returning a 400 Bad Request error from the Korea Exchange (KRX) data API (data.krx.co.kr/comm/bldAttendant/getJsonData.cmd). Starting December 27, 2025, KRX enforced session-based authentication for its data marketplace, breaking all unauthenticated POST requests. The current code in app/services/krx.py creates a fresh httpx.AsyncClient per request without any session/cookie management or login flow, which now results in 400 Bad Request responses (with "LOGOUT" body). This fix requires implementing a persistent authenticated session manager for KRX API calls.

Summary by CodeRabbit

Release Notes

  • New Features

    • Implemented session-based authentication for KRX API integration to resolve 400 Bad Request errors.
    • Added support for KRX member credentials configuration via environment variables.
    • Automatic session re-authentication on login expiration.
  • Bug Fixes

    • Enhanced reliability of KRX API calls with improved error handling and recovery mechanisms.

robin-watcha and others added 5 commits March 5, 2026 15:17
…l fields

Add KRX credentials to Settings class and env.example for KRX data system integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ices/krx

Implement persistent authenticated session manager for KRX data API:
- KRXSessionManager with lazy httpx.AsyncClient creation
- Cookie-based login flow (MDCCOMS001D1.cmd) with duplicate login handling
- Session expiry detection (400/LOGOUT) with automatic re-auth
- 403 rate limiting with exponential backoff (3 retries, base 5s)
- Concurrent login prevention via asyncio.Lock
- Graceful fallback to unauthenticated on missing/invalid credentials
- Module-level singleton (_krx_session)
- Refactored _fetch_krx_data and _fetch_max_working_date to delegate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tdown lifecycle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…n management

Tests cover login success/failure/duplicate, re-auth on LOGOUT,
no-credentials fallback, session reuse, concurrent login lock,
fetch delegation, and session close.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All 103 tests pass (test_services_krx.py + test_mcp_screen_stocks.py).
Linting clean on krx.py. No regressions from KRXSessionManager refactoring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

📝 Walkthrough

Walkthrough

This change implements a session-based authentication flow for the KRX API to resolve 400 Bad Request errors. It introduces a KRXSessionManager class for handling login, session persistence, and automatic re-authentication, alongside configuration for KRX credentials, cleanup logic in the application shutdown hook, and comprehensive test coverage.

Changes

Cohort / File(s) Summary
Implementation Planning
.auto-claude/specs/001-fix-github-issue-195-auto-trader/implementation_plan.json
Outlines a four-phase plan to fix KRX API 400 errors via session-based authentication, including configuration setup, KRXSessionManager implementation, testing strategy, and code quality checks with verification steps and acceptance criteria.
Configuration & Credentials
app/core/config.py, env.example
Adds two optional environment variables and Settings fields (krx_member_id, krx_password) to support KRX credential configuration.
KRX Session Management
app/services/krx.py
Introduces KRXSessionManager class with login flow, session persistence, automatic re-authentication on 400/LOGOUT responses, and exponential backoff on 403. Refactors _fetch_krx_data and _get_max_working_date to delegate to the centralized session manager instead of direct per-call httpx usage.
Application Lifecycle
app/main.py
Adds cleanup logic in the shutdown hook to gracefully close the KRX session, including error handling to preserve existing shutdown flow.
Test Coverage
tests/test_services_krx.py
Adds extensive test suite for KRXSessionManager covering successful login, fallback modes, automatic re-authentication, session reuse, concurrency safety with asyncio.Lock, and proper client cleanup.

Sequence Diagram(s)

sequenceDiagram
    participant App as App Startup
    participant Config as Config/Settings
    participant KRXMgr as KRXSessionManager
    participant KRXApi as KRX API
    participant Shutdown as App Shutdown

    App->>Config: Load krx_member_id, krx_password
    App->>KRXMgr: Create _krx_session singleton
    
    Note over KRXMgr: Ready (not yet connected)
    
    App->>App: Register shutdown handler
    
    rect rgba(100, 150, 200, 0.5)
        Note over App,KRXApi: API Call Flow
        App->>KRXMgr: fetch_data()
        KRXMgr->>KRXMgr: _ensure_session()
        alt Credentials available
            KRXMgr->>KRXApi: POST /Login (CD001)
            KRXApi-->>KRXMgr: Session cookie + auth state
        else No credentials
            KRXMgr->>KRXMgr: Proceed unauthenticated
        end
        KRXMgr->>KRXApi: GET /Data
        alt 400 Bad Request
            KRXMgr->>KRXMgr: Trigger re-authentication
            KRXMgr->>KRXApi: POST /Login (retry)
            KRXApi-->>KRXMgr: New session
            KRXMgr->>KRXApi: GET /Data (retry)
        else 403 Forbidden
            KRXMgr->>KRXMgr: Exponential backoff + retry
        end
        KRXApi-->>KRXMgr: Data response
        KRXMgr-->>App: Cached/parsed data
    end
    
    rect rgba(200, 150, 100, 0.5)
        Note over Shutdown,KRXMgr: Shutdown Flow
        Shutdown->>KRXMgr: close()
        KRXMgr->>KRXApi: Release persistent session
        KRXMgr->>KRXMgr: Reset state
        KRXMgr-->>Shutdown: Cleanup complete
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A session springs to life, so neat,
With cookies stored and auth complete!
When logins fail, we rise again,
With locks and retries—KRX's friend.
Through shutdown's gate, we close with care! 🔐

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main change: fixing KRX API 400 Bad Request errors by implementing session-based authentication.
Docstring Coverage ✅ Passed Docstring coverage is 86.96% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch auto-claude/001-fix-github-issue-195-auto-trader

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 5, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
E Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@claude
Copy link

claude bot commented Mar 5, 2026

Code Review: Fix KRX API 400 Bad Request - Session-Based Authentication

Good overall implementation. The KRXSessionManager design is solid and the test coverage is comprehensive. Here are my findings:


🔴 Issues

1. .auto-claude/specs/ should not be committed

The file .auto-claude/specs/001-fix-github-issue-195-auto-trader/implementation_plan.json is an internal planning artifact and should not be part of the repository. It also embeds local machine paths (/Users/robin/PycharmProjects/auto_trader) which are environment-specific. Add this directory to .gitignore.

2. Mixed HTTP/HTTPS — cookies may not be transmitted

The login flow acquires session cookies via HTTPS:

KRX_LOGIN_URL = "https://data.krx.co.kr/comm/login/MDCCOMS001D1.cmd"
KRX_BASE_URL  = "https://data.krx.co.kr/"

But the actual API calls go over HTTP:

KRX_API_URL      = "http://data.krx.co.kr/comm/bldAttendant/getJsonData.cmd"
KRX_RESOURCE_URL = "http://data.krx.co.kr/comm/bldAttendant/executeForResourceBundle.cmd"

Session cookies flagged Secure will not be sent over HTTP, causing authentication to silently fail even after a successful login. All URLs should consistently use HTTPS, or you need to verify KRX does not set Secure cookies.

3. _auth_failed is a permanent one-way flag

Once set to True, _auth_failed is never reset by _ensure_session or fetch_data — only by close(). This means a transient network error during startup permanently disables authentication for the lifetime of the process. Consider either resetting _auth_failed after a cooldown period, or adding a mechanism to reset the session.


🟡 Minor Issues

4. return [] at the end of fetch_data is dead code

After exhausting 403 retries, the code falls through to response.raise_for_status() which always raises HTTPStatusError. The final return [] is unreachable. Remove it to avoid confusion:

# Remove this line at the end of fetch_data
return []

5. Timeout regression in fetch_resource

The original _fetch_max_working_date used timeout=10. The session client uses timeout=30. This is a minor behavioral change that could slow down startup if KRX is unresponsive.

6. fetch_resource has no authentication error handling

Unlike fetch_data, fetch_resource has no retry or re-auth logic. If the working-date lookup fails due to session expiry, it raises immediately with no recovery path.


✅ Good Practices

  • asyncio.Lock correctly prevents concurrent login race conditions
  • Credentials are never logged in plaintext (only body[:200] of the response)
  • Graceful fallback when no credentials are configured
  • Proper session cleanup in app/main.py shutdown lifecycle
  • Public API (fetch_stock_all, fetch_etf_all, fetch_valuation_all) is unchanged
  • Test coverage is thorough — all major flows (success, CD011 duplicate, failure, re-auth, no credentials, session reuse, concurrency, delegation, close) are covered

Summary

The core logic is correct and well-structured. Please address:

  1. Remove .auto-claude/specs/ from the repository and .gitignore it
  2. Unify all KRX URLs to HTTPS to ensure cookies are transmitted correctly
  3. Consider making _auth_failed recoverable or add a reset mechanism

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/test_services_krx.py (1)

1013-1014: Consider adding @pytest.mark.unit marker for test categorization.

Per coding guidelines, tests should use markers (@pytest.mark.unit, @pytest.mark.integration, @pytest.mark.slow) for categorization. The existing tests in this file don't have explicit markers either, so this is consistent with the current pattern but worth noting for future improvements.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_services_krx.py` around lines 1013 - 1014, Add the pytest unit
marker to the async test by decorating the test_session_manager_login_success
coroutine with `@pytest.mark.unit` (i.e., add the `@pytest.mark.unit` decorator
directly above the existing `@pytest.mark.asyncio` on the
test_session_manager_login_success function) so it is categorized as a unit
test; ensure pytest is imported if not already.
app/services/krx.py (1)

129-197: Consider suppressing lint warnings for KRX API parameter names.

The parameter names mktId, trdDd, and idxIndClssCd match the actual KRX API parameter names, which improves code clarity when debugging API calls. The SonarCloud warnings about snake_case can be addressed with a targeted suppression comment if desired.

Regarding cognitive complexity (21 vs allowed 15): the method handles distinct concerns (session management, 400/LOGOUT re-auth, 403 rate limiting). Consider extracting helper methods only if this becomes harder to maintain.

💡 Optional: Suppress parameter naming warnings
     async def fetch_data(
         self,
         bld: str,
-        mktId: str | None = None,
-        trdDd: str | None = None,
-        idxIndClssCd: str | None = None,
+        mktId: str | None = None,  # noqa: N803 - matches KRX API param
+        trdDd: str | None = None,  # noqa: N803 - matches KRX API param
+        idxIndClssCd: str | None = None,  # noqa: N803 - matches KRX API param
     ) -> list[dict[str, Any]]:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/services/krx.py` around lines 129 - 197, The Sonar/flake8 naming warnings
come from using KRX API parameter names (mktId, trdDd, idxIndClssCd) in
fetch_data; suppress the naming rule for this function by adding a targeted lint
suppression comment (e.g., append "# noqa: N802" or the project's configured
rule ID) to the fetch_data definition line so the params can remain as-is, and
leave the method logic (session handling, 400/LOGOUT re-auth, 403 backoff)
unchanged unless future complexity warrants extracting helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_services_krx.py`:
- Around line 1200-1210: Remove the unused assignment to original_login which
causes a lint error: delete the line "original_login = manager._login" in the
test where you replace manager._login with the async counting_login; keep the
counting_login closure (which references login_call_count and sets
manager._authenticated) and the reassignment "manager._login = counting_login"
as-is so the test behavior is unchanged.

---

Nitpick comments:
In `@app/services/krx.py`:
- Around line 129-197: The Sonar/flake8 naming warnings come from using KRX API
parameter names (mktId, trdDd, idxIndClssCd) in fetch_data; suppress the naming
rule for this function by adding a targeted lint suppression comment (e.g.,
append "# noqa: N802" or the project's configured rule ID) to the fetch_data
definition line so the params can remain as-is, and leave the method logic
(session handling, 400/LOGOUT re-auth, 403 backoff) unchanged unless future
complexity warrants extracting helpers.

In `@tests/test_services_krx.py`:
- Around line 1013-1014: Add the pytest unit marker to the async test by
decorating the test_session_manager_login_success coroutine with
`@pytest.mark.unit` (i.e., add the `@pytest.mark.unit` decorator directly above the
existing `@pytest.mark.asyncio` on the test_session_manager_login_success
function) so it is categorized as a unit test; ensure pytest is imported if not
already.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a68c4f8c-3f77-405e-b4d1-84851bb2f23d

📥 Commits

Reviewing files that changed from the base of the PR and between 3c54e58 and d597e1f.

📒 Files selected for processing (6)
  • .auto-claude/specs/001-fix-github-issue-195-auto-trader/implementation_plan.json
  • app/core/config.py
  • app/main.py
  • app/services/krx.py
  • env.example
  • tests/test_services_krx.py

Comment on lines +1200 to +1210
login_call_count = 0
original_login = manager._login

async def counting_login():
nonlocal login_call_count
login_call_count += 1
# Simulate slow login
await asyncio.sleep(0.01)
manager._authenticated = True

manager._login = counting_login
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused variable original_login - pipeline failure.

The variable original_login is assigned but never used, causing a Ruff F841 lint error and pipeline failure.

🔧 Proposed fix
         login_call_count = 0
-        original_login = manager._login

         async def counting_login():
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
login_call_count = 0
original_login = manager._login
async def counting_login():
nonlocal login_call_count
login_call_count += 1
# Simulate slow login
await asyncio.sleep(0.01)
manager._authenticated = True
manager._login = counting_login
login_call_count = 0
async def counting_login():
nonlocal login_call_count
login_call_count += 1
# Simulate slow login
await asyncio.sleep(0.01)
manager._authenticated = True
manager._login = counting_login
🧰 Tools
🪛 GitHub Actions: Test

[error] 1201-1201: F841 Local variable original_login is assigned to but never used. Remove assignment to unused variable original_login.

🪛 GitHub Check: SonarCloud Code Analysis

[warning] 1201-1201: Remove the unused local variable "original_login".

See more on https://sonarcloud.io/project/issues?id=mgh3326_auto_trader&issues=AZy87uqSdpTYh2HpkTi8&open=AZy87uqSdpTYh2HpkTi8&pullRequest=196

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_services_krx.py` around lines 1200 - 1210, Remove the unused
assignment to original_login which causes a lint error: delete the line
"original_login = manager._login" in the test where you replace manager._login
with the async counting_login; keep the counting_login closure (which references
login_call_count and sets manager._authenticated) and the reassignment
"manager._login = counting_login" as-is so the test behavior is unchanged.

@mgh3326 mgh3326 merged commit 13e42e2 into main Mar 5, 2026
11 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants