Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
182 lines
6.7 KiB
Python
182 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Batch align error responses in GoChat handlers to Chatwoot's error code convention.
|
|
|
|
Chatwoot conventions:
|
|
- Record not found → 404
|
|
- Validation/create/update/delete failures → 422 UnprocessableEntity
|
|
- Auth failures → 401 Unauthorized
|
|
- Permission failures → 403 Forbidden
|
|
- Request format/bind errors → 400 BadRequest
|
|
- Conflict/duplicate → 409 Conflict
|
|
- Internal server errors should be rare (only for unexpected DB/infra failures)
|
|
|
|
Current GoChat pattern: most errors use 500 InternalServerError for business logic failures.
|
|
Target: replace 500 with appropriate 422/404/401/403 based on error message content.
|
|
"""
|
|
|
|
import re
|
|
import os
|
|
import sys
|
|
|
|
# Error message → target HTTP status mapping
|
|
# Pattern: if the gin.H{"error": "..."} message matches a keyword, use the mapped status
|
|
MESSAGE_STATUS_MAP = {
|
|
# 404 patterns
|
|
'not found': 'http.StatusNotFound',
|
|
'does not exist': 'http.StatusNotFound',
|
|
|
|
# 401 patterns
|
|
'unauthorized': 'http.StatusUnauthorized',
|
|
'authentication': 'http.StatusUnauthorized',
|
|
'invalid token': 'http.StatusUnauthorized',
|
|
|
|
# 403 patterns
|
|
'forbidden': 'http.StatusForbidden',
|
|
'not allowed': 'http.StatusForbidden',
|
|
'permission': 'http.StatusForbidden',
|
|
'access denied': 'http.StatusForbidden',
|
|
'does not belong': 'http.StatusForbidden', # "does not belong to this account"
|
|
|
|
# 409 patterns
|
|
'already exists': 'http.StatusConflict',
|
|
'duplicate': 'http.StatusConflict',
|
|
|
|
# 422 patterns (default for business logic failures)
|
|
'failed to': 'http.StatusUnprocessableEntity', # "failed to create/update/delete/list"
|
|
'invalid': 'http.StatusUnprocessableEntity',
|
|
'cannot': 'http.StatusUnprocessableEntity',
|
|
'must': 'http.StatusUnprocessableEntity',
|
|
'required': 'http.StatusUnprocessableEntity',
|
|
'missing': 'http.StatusUnprocessableEntity',
|
|
}
|
|
|
|
def classify_error_status(error_msg: str) -> str:
|
|
"""Classify an error message into the appropriate HTTP status code constant."""
|
|
lower_msg = error_msg.lower()
|
|
|
|
# Check in priority order — specific patterns first
|
|
for pattern, status in MESSAGE_STATUS_MAP.items():
|
|
if pattern in lower_msg:
|
|
return status
|
|
|
|
# Default: 422 for business logic failures that were previously 500
|
|
return 'http.StatusUnprocessableEntity'
|
|
|
|
def process_file(filepath: str) -> dict:
|
|
"""Process a single Go file, replacing 500 errors with appropriate status codes."""
|
|
with open(filepath, 'r') as f:
|
|
content = f.read()
|
|
|
|
original_content = content
|
|
changes = []
|
|
|
|
# Pattern: c.JSON(http.StatusInternalServerError, gin.H{"error": "..."})
|
|
# Replace with appropriate status code based on error message
|
|
pattern = re.compile(
|
|
r'c\.JSON\(http\.StatusInternalServerError,\s*gin\.H\{'
|
|
r'"error":\s*"([^"]+)"'
|
|
r'\}\)'
|
|
)
|
|
|
|
def replacer(match):
|
|
error_msg = match.group(1)
|
|
new_status = classify_error_status(error_msg)
|
|
old = match.group(0)
|
|
new = f'c.JSON({new_status}, gin.H{{"error": "{error_msg}"}})'
|
|
changes.append({
|
|
'line': content[:match.start()].count('\n') + 1,
|
|
'old': old,
|
|
'new': new,
|
|
'status': new_status.replace('http.Status', ''),
|
|
})
|
|
return new
|
|
|
|
content = pattern.sub(replacer, content)
|
|
|
|
# Also handle: c.JSON(http.StatusInternalServerError, response.APIResponse{...})
|
|
pattern2 = re.compile(
|
|
r'c\.JSON\(http\.StatusInternalServerError,\s*response\.APIResponse\{[^}]+\}\)'
|
|
)
|
|
# For these, just replace status code — can't easily classify the message
|
|
content2 = re.sub(
|
|
r'c\.JSON\(http\.StatusInternalServerError,\s*response\.APIResponse',
|
|
'c.JSON(http.StatusUnprocessableEntity, response.APIResponse',
|
|
content
|
|
)
|
|
if content2 != content:
|
|
changes.append({'line': 0, 'old': '500 APIResponse', 'new': '422 APIResponse', 'status': 'UnprocessableEntity'})
|
|
content = content2
|
|
|
|
if content != original_content:
|
|
with open(filepath, 'w') as f:
|
|
f.write(content)
|
|
|
|
return {'file': filepath, 'changes': changes, 'total_changes': len(changes)}
|
|
|
|
def main():
|
|
handler_dir = '/home/yanghao05/Workspace/gochat/internal/handler'
|
|
|
|
# Find all handler Go files (exclude tests)
|
|
go_files = []
|
|
for root, dirs, files in os.walk(handler_dir):
|
|
for f in files:
|
|
if f.endswith('.go') and not f.endswith('_test.go'):
|
|
go_files.append(os.path.join(root, f))
|
|
|
|
total_changes = 0
|
|
status_distribution = {}
|
|
|
|
for filepath in go_files:
|
|
result = process_file(filepath)
|
|
if result['total_changes'] > 0:
|
|
total_changes += result['total_changes']
|
|
for change in result['changes']:
|
|
status = change['status']
|
|
status_distribution[status] = status_distribution.get(status, 0) + 1
|
|
print(f" {filepath}:{change['line']} → {status}: {change['new']}")
|
|
|
|
print(f"\n=== Summary ===")
|
|
print(f"Files processed: {len(go_files)}")
|
|
print(f"Total changes: {total_changes}")
|
|
print(f"Status distribution:")
|
|
for status, count in sorted(status_distribution.items()):
|
|
print(f" {status}: {count}")
|
|
|
|
# Also handle 400 → 422 for "failed to" business logic errors
|
|
# Pattern: c.JSON(http.StatusBadRequest, gin.H{"error": "failed to..."})
|
|
print(f"\n=== Phase 2: 400 → 422 for business logic errors ===")
|
|
|
|
for filepath in go_files:
|
|
with open(filepath, 'r') as f:
|
|
content = f.read()
|
|
original = content
|
|
|
|
# Replace 400 with 422 for "failed to" errors (business logic, not request format)
|
|
pattern = re.compile(
|
|
r'c\.JSON\(http\.StatusBadRequest,\s*gin\.H\{'
|
|
r'"error":\s*"([^"]+)"'
|
|
r'\}\)'
|
|
)
|
|
|
|
changes_400 = []
|
|
def replacer400(match):
|
|
error_msg = match.group(1)
|
|
lower = error_msg.lower()
|
|
# Only replace if it's a business logic failure, not a request format error
|
|
if 'failed to' in lower or 'cannot' in lower or 'must' in lower:
|
|
new = f'c.JSON(http.StatusUnprocessableEntity, gin.H{{"error": "{error_msg}"}})'
|
|
changes_400.append({'msg': error_msg, 'status': 'UnprocessableEntity'})
|
|
return new
|
|
return match.group(0)
|
|
|
|
content = pattern.sub(replacer400, content)
|
|
|
|
if content != original:
|
|
with open(filepath, 'w') as f:
|
|
f.write(content)
|
|
for c in changes_400:
|
|
print(f" {filepath}: 400→422: {c['msg']}")
|
|
total_changes += len(changes_400)
|
|
|
|
if __name__ == '__main__':
|
|
main() |