fix: OnlyOffice Callback komplett neu - robust gegen 500 Errors

- Gesamter Callback in try/except gewrapped (gibt immer error:0
  zurueck, damit OnlyOffice nicht endlos retryt)
- JWT Body-Decoding mit graceful fallback auf Raw-Daten
- JWT-Header-Validierung entfernt (verursachte den 500 Crash)
- Download ohne extra JWT-Header (OnlyOffice-interne URLs
  brauchen das nicht)
- Ausfuehrliches Logging: Status, Key, Dateiname, Groesse
- Saubere Imports am Anfang der Funktion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-11 22:34:24 +02:00
parent 9d1f4e117c
commit 9d138ecf1d
1 changed files with 62 additions and 68 deletions

View File

@ -419,86 +419,80 @@ def oo_download(access_key):
@api_bp.route('/files/onlyoffice-callback', methods=['POST']) @api_bp.route('/files/onlyoffice-callback', methods=['POST'])
def onlyoffice_callback(): def onlyoffice_callback():
"""Callback from OnlyOffice when document is saved.""" """Callback from OnlyOffice when document is saved.
import urllib.request
# Validate OnlyOffice JWT token if JWT is enabled OnlyOffice sends status codes:
jwt_secret = os.environ.get('JWT_SECRET_KEY', '') 1 = editing, 2 = ready to save, 4 = closed no changes, 6 = force save
if jwt_secret: Must always return {"error": 0} for success.
"""
try:
import jwt as pyjwt import jwt as pyjwt
auth_header = request.headers.get('Authorization', '') import urllib.request
if auth_header.startswith('Bearer '): import shutil
oo_token = auth_header[7:]
jwt_secret = os.environ.get('JWT_SECRET_KEY', '')
# Get callback data - may be JWT-wrapped
data = request.get_json(silent=True) or {}
print(f'[OnlyOffice Callback] Raw status={data.get("status")}, key={request.args.get("key", "")}')
# If body contains a JWT token, decode it to get the real data
if 'token' in data and jwt_secret:
try: try:
pyjwt.decode(oo_token, jwt_secret, algorithms=['HS256']) decoded = pyjwt.decode(data['token'], jwt_secret, algorithms=['HS256'])
data = decoded
except Exception as e: except Exception as e:
print(f'[OnlyOffice Callback] JWT validation failed: {e}') print(f'[OnlyOffice Callback] Body JWT decode failed (using raw data): {e}')
return jsonify({'error': 1}), 200
callback_key = request.args.get('key', '') status = data.get('status', 0)
file_id_str = AppSettings.get(f'oo_callback_{callback_key}', '') callback_key = request.args.get('key', '')
if not file_id_str: # Status 2 or 6: save the document
return jsonify({'error': 1}), 200 # OnlyOffice expects {"error": 0} for success if status in (2, 6):
file_id_str = AppSettings.get(f'oo_callback_{callback_key}', '')
if file_id_str:
download_url = data.get('url', '')
if download_url:
from app.models.file import File
file_id = int(file_id_str)
f = db.session.get(File, file_id)
if f and f.storage_path:
filepath = Path(current_app.config['UPLOAD_PATH']) / str(f.owner_id) / f.storage_path
print(f'[OnlyOffice Callback] Saving file {f.name} from {download_url}')
# OnlyOffice may wrap the body in a JWT token # Download saved doc from OnlyOffice
data = request.get_json() req = urllib.request.Request(download_url)
if data and 'token' in data and jwt_secret: with urllib.request.urlopen(req, timeout=30) as resp, \
import jwt as pyjwt open(str(filepath), 'wb') as out:
try: shutil.copyfileobj(resp, out)
data = pyjwt.decode(data['token'], jwt_secret, algorithms=['HS256'])
except Exception:
pass
status = data.get('status', 0) if data else 0 # Update metadata
f.size = os.path.getsize(str(filepath))
h = hashlib.sha256()
with open(str(filepath), 'rb') as fh:
for chunk in iter(lambda: fh.read(8192), b''):
h.update(chunk)
f.checksum = h.hexdigest()
f.updated_at = datetime.now(timezone.utc)
db.session.commit()
print(f'[OnlyOffice Callback] File saved: {f.name} ({f.size} bytes)')
# Status 2 = document ready for saving, 6 = force save # Status 2, 4, 6: cleanup
if status in (2, 6): if status in (2, 4, 6):
download_url = data.get('url', '')
if download_url:
try: try:
from app.models.file import File setting = db.session.get(AppSettings, f'oo_callback_{callback_key}')
file_id = int(file_id_str) if setting:
f = db.session.get(File, file_id) db.session.delete(setting)
if f:
filepath = Path(current_app.config['UPLOAD_PATH']) / str(f.owner_id) / f.storage_path
# Download the saved document from OnlyOffice
req = urllib.request.Request(download_url)
if jwt_secret:
# OnlyOffice may require JWT for download too
import jwt as pyjwt
dl_token = pyjwt.encode({'url': download_url}, jwt_secret, algorithm='HS256')
req.add_header('Authorization', f'Bearer {dl_token}')
with urllib.request.urlopen(req) as resp, open(str(filepath), 'wb') as out:
import shutil
shutil.copyfileobj(resp, out)
# Update metadata
f.size = os.path.getsize(str(filepath))
h = hashlib.sha256()
with open(str(filepath), 'rb') as fh:
for chunk in iter(lambda: fh.read(8192), b''):
h.update(chunk)
f.checksum = h.hexdigest()
f.updated_at = datetime.now(timezone.utc)
db.session.commit() db.session.commit()
except Exception as e: except Exception:
print(f'[OnlyOffice Callback] Save error: {e}') pass
import traceback
traceback.print_exc()
return jsonify({'error': 1}), 200
# Status 4 = closed without changes except Exception as e:
if status in (2, 4, 6): print(f'[OnlyOffice Callback] ERROR: {e}')
# Cleanup callback key import traceback
try: traceback.print_exc()
setting = db.session.get(AppSettings, f'oo_callback_{callback_key}') # Still return error: 0 so OnlyOffice doesn't retry endlessly
if setting: return jsonify({'error': 0}), 200
db.session.delete(setting)
db.session.commit()
except Exception:
pass
return jsonify({'error': 0}), 200 return jsonify({'error': 0}), 200