Skip to content

Commit cc06a33

Browse files
fix: handle redis outages and unify share update error flow
1 parent 752939d commit cc06a33

3 files changed

Lines changed: 49 additions & 27 deletions

File tree

app/routes/share.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ def create_share_link():
2626
if not data or not isinstance(data, dict):
2727
return jsonify({'error': 'No data provided or invalid format'}), 400
2828

29+
student_no = session.get('student_no')
30+
if not student_no:
31+
return jsonify({'error': 'Authentication required'}), 401
32+
33+
redis_client = current_app.config.get('REDIS_CLIENT')
34+
if redis_client is None:
35+
return jsonify({'error': 'Share service unavailable'}), 503
36+
2937
# Turnstile 人機驗證
3038
ts_ok, ts_err = verify_turnstile_token(
3139
data.get('turnstile_token'),
@@ -35,7 +43,6 @@ def create_share_link():
3543
return jsonify({'error': ts_err}), 403
3644

3745
# 速率限制檢查(在 Turnstile 之後)
38-
redis_client = current_app.config.get('REDIS_CLIENT')
3946
if redis_client:
4047
try:
4148
# 較寬鬆的速率限制:每小時 (3600 秒) 10 次
@@ -60,12 +67,7 @@ def create_share_link():
6067
if not valid:
6168
return jsonify({'error': err}), 400
6269

63-
student_no = session.get('student_no')
64-
if not student_no:
65-
return jsonify({'error': 'Authentication required'}), 401
66-
6770
share_id = generate_share_id()
68-
redis_client = current_app.config['REDIS_CLIENT']
6971
share_ttl = current_app.config['SHARE_TTL']
7072
write_shared_data(redis_client, share_id, cleaned, share_ttl)
7173
write_share_metadata(redis_client, share_id, student_no, share_ttl)
@@ -85,7 +87,9 @@ def update_share_link(share_id):
8587
if not data or not isinstance(data, dict):
8688
return jsonify({'error': 'No data provided or invalid format'}), 400
8789

88-
redis_client = current_app.config['REDIS_CLIENT']
90+
redis_client = current_app.config.get('REDIS_CLIENT')
91+
if redis_client is None:
92+
return jsonify({'error': 'Share service unavailable'}), 503
8993

9094
requester_student_no = session.get('student_no')
9195
if requester_student_no is None:

frontend/share.js

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,20 @@ export function getActiveShareId() {
1616

1717
export async function updateActiveShare(gradesData) {
1818
const shareId = getActiveShareId();
19-
if (!shareId || !gradesData) return;
20-
21-
try {
22-
const res = await fetch(`/api/share/${shareId}`, {
23-
method: 'PUT',
24-
headers: { 'Content-Type': 'application/json' },
25-
credentials: 'include',
26-
body: JSON.stringify(gradesData)
27-
});
19+
if (!shareId || !gradesData) return { attempted: false, ok: false, status: null };
2820

29-
if (!res.ok) {
30-
if (res.status === 404 || res.status === 403) {
31-
localStorage.removeItem(ACTIVE_SHARE_ID_KEY);
32-
}
33-
return;
34-
}
35-
} catch (error) {
36-
console.error('更新分享失敗:', error);
21+
const res = await fetch(`/api/share/${shareId}`, {
22+
method: 'PUT',
23+
headers: { 'Content-Type': 'application/json' },
24+
credentials: 'include',
25+
body: JSON.stringify(gradesData)
26+
});
27+
28+
if (!res.ok && (res.status === 404 || res.status === 403)) {
29+
localStorage.removeItem(ACTIVE_SHARE_ID_KEY);
3730
}
31+
32+
return { attempted: true, ok: res.ok, status: res.status };
3833
}
3934

4035
export function setupShareFeature() {

tests/backend/test_share_routes.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import json
2-
import os
3-
42
import pytest
53

64
from app import create_app
@@ -40,7 +38,7 @@ def ttl(self, key):
4038

4139
@pytest.fixture
4240
def client(monkeypatch):
43-
os.environ['APP_ENV'] = 'testing'
41+
monkeypatch.setenv('APP_ENV', 'testing')
4442
app = create_app()
4543
app.config['TESTING'] = True
4644
app.config['SHARE_TTL'] = 7200
@@ -153,3 +151,28 @@ def test_share_update_rate_limited(client):
153151
assert last_response is not None
154152
assert last_response.status_code == 429
155153
assert 'Retry-After' in last_response.headers
154+
155+
156+
def test_share_create_returns_503_when_redis_unavailable(client):
157+
with client.session_transaction() as sess:
158+
sess['student_no'] = 'A123'
159+
160+
client.application.config['REDIS_CLIENT'] = None
161+
create_res = client.post('/api/share', json=_share_payload())
162+
163+
assert create_res.status_code == 503
164+
assert create_res.get_json()['error'] == 'Share service unavailable'
165+
166+
167+
def test_share_update_returns_503_when_redis_unavailable(client):
168+
with client.session_transaction() as sess:
169+
sess['student_no'] = 'A123'
170+
171+
create_res = client.post('/api/share', json=_share_payload())
172+
share_id = create_res.get_json()['id']
173+
174+
client.application.config['REDIS_CLIENT'] = None
175+
update_res = client.put(f'/api/share/{share_id}', json=_share_payload())
176+
177+
assert update_res.status_code == 503
178+
assert update_res.get_json()['error'] == 'Share service unavailable'

0 commit comments

Comments
 (0)