# Tips, Patterns, and Gotchas

> Practical patterns discovered from real-world Forgejo API usage.

---

## 1. Multi-Identity Authentication

When using the Forgejo API with automation, you often need multiple identities. For example, a bot creates PRs and a separate reviewer bot posts reviews so that the reviewer is a different person from the PR author. This is achieved by using different PATs:

```bash
# Bot 1 creates the PR
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls" \
  -H "Authorization: token ${BOT_PAT}" \
  -H "Content-Type: application/json" \
  -d '{"title": "feat: new feature", "head": "{head_branch}", "base": "{base_branch}"}'

# Bot 2 (reviewer identity) posts the review
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews" \
  -H "Authorization: token ${REVIEWER_PAT}" \
  -H "Content-Type: application/json" \
  -d '{"event": "APPROVED", "body": "Looks good!"}'
```

This is critical when branch protection requires approvals from a different user than the PR author.

---

## 2. PRs Are Issues

In Forgejo, pull requests are a superset of issues. Every PR is also an issue. This means:
- Issue comment endpoints (`/repos/{owner}/{repo}/issues/{index}/comments`) work for PRs
- Issue label endpoints work for PRs (same index number)
- Issue reaction endpoints work for PRs
- Issue timeline endpoints work for PRs
- The issue index and PR index are the same number

```bash
# Add a comment to PR #42 using the issue endpoint
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/comments" \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{"body": "This comment on the PR uses the issue endpoint"}'
```

---

## 3. Editing Issue Body for Subtask Checkboxes

A common pattern is updating markdown checkboxes in issue bodies to track subtask completion:

```bash
# 1. Get the current issue body
ISSUE=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" \
  -H "Authorization: token ${FORGEJO_PAT}")
BODY=$(echo "$ISSUE" | jq -r '.body')

# 2. Update checkboxes from [ ] to [x] for completed items
UPDATED_BODY=$(echo "$BODY" | sed 's/- \[ \] Implement login/- [x] Implement login/')

# 3. PATCH the issue with the updated body
curl -s -X PATCH "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg body "$UPDATED_BODY" '{body: $body}')"
```

Important: Use `jq` for JSON encoding the body to handle special characters, newlines, and quotes safely.

---

## 4. PR Auto-Close Issues

Forgejo automatically closes linked issues when a PR is merged if the PR body contains closing keywords:
- `Closes #123`
- `Fixes #123`
- `Resolves #123`

These must be on their own line or in a recognizable format in the PR body. Multiple issues can be closed:

```
Closes #123
Closes #124
```

---

## 5. Pagination Best Practices

- Always use `limit=50` (the maximum) for efficiency
- Check `len(results) == limit` to determine if more pages exist
- Some endpoints return data inside wrapper objects (e.g., `/repos/search` returns `{"data": [...], "ok": true}`)
- The `X-Total-Count` header may be present on some list responses

---

## 6. Rate Limiting and Error Handling

- Forgejo returns `429 Too Many Requests` when rate limited
- Always check HTTP status codes (see full table below)
- The error response format is: `{"message": "error description", "url": "swagger url"}`

### Full HTTP Status Code Reference

| Code | Meaning | When It Happens |
|------|---------|------------------|
| 200 | OK | Successful GET, PATCH, some POST |
| 201 | Created | Successful POST that creates a resource |
| 204 | No Content | Successful DELETE, or check-exists endpoints (e.g., merge check) |
| 304 | Not Modified | Conditional request, no changes (e.g., subscription unchanged) |
| 400 | Bad Request | Invalid parameters, malformed body |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but lacking permission |
| 404 | Not Found | Resource doesn't exist, or not-merged check |
| 405 | Method Not Allowed | Operation blocked (e.g., deleting protected tag) |
| 409 | Conflict | Merge conflict, duplicate resource, optimistic lock failure |
| 412 | Precondition Failed | `updated_at` in body doesn't match server (stale edit protection) |
| 413 | Request Entity Too Large | Quota exceeded, file too large |
| 422 | Unprocessable Entity | Validation error (e.g., invalid field values) |
| 423 | Locked | Repo is archived, in transfer, or otherwise locked |
| 429 | Too Many Requests | Rate limited |
| 500 | Internal Server Error | Server bug |

**412 Precondition Failed** deserves special attention: when editing issues or PRs with PATCH, you can include an `updated_at` field in the body. If the server's `updated_at` is newer, it returns 412 to prevent overwriting concurrent edits. This is an optimistic concurrency control mechanism.

**423 Locked** is returned when trying to modify a repository that is archived, in the middle of a transfer, or otherwise locked for modifications.

---

## 7. Webhook Delivery Debugging

When a webhook doesn't fire, check:
1. The hook is active
2. The event type matches
3. The branch filter matches (if set)
4. Check the delivery log in the web UI: Settings → Webhooks → click hook → Recent Deliveries

---

## 8. Working with Base64 Content

File content endpoints use base64 encoding:

```bash
# Encode content for upload
echo -n "Hello World" | base64
# SGVsbG8gV29ybGQ=

# Decode content from download
echo "SGVsbG8gV29ybGQ=" | base64 -d
# Hello World
```

---

## 9. Handling Large Diffs and Files

- PR file list can be paginated with `skip-to` parameter (skip to a specific file)
- Use `whitespace` parameter to ignore whitespace changes
- For large diffs, use the `.diff` or `.patch` format endpoints instead of JSON
- The `default_max_blob_size` setting (from `GET /settings/api`) controls the maximum file size returned via content API

---

## 10. Optimistic Locking for Merges

When merging a PR, use `head_commit_id` to prevent race conditions:

```bash
# Get current head SHA
HEAD=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}" \
  -H "Authorization: token ${FORGEJO_PAT}" | jq -r '.head.sha')

# Merge with optimistic lock
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}/merge" \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d "{\"Do\": \"squash\", \"head_commit_id\": \"${HEAD}\", \"delete_branch_after_merge\": true}"
```

If someone pushes new commits after you read the SHA, the merge will fail with 409 instead of silently merging stale code.

---

## 11. Finding a PR's Linked Issue

Forgejo doesn't have a dedicated "linked issue" field. Instead:
- Parse the PR body for `Closes #N`, `Fixes #N`, `Resolves #N`
- Use the PR timeline to find cross-reference events
- Check issue dependencies: `GET /repos/{owner}/{repo}/issues/{index}/dependencies`

---

## 12. Commit Status Context Naming

CI systems create commit statuses with a `context` string (e.g., `"CI / build (pull_request)"`). Branch protection rules can require specific contexts. The `target_url` in the status often contains a relative URL like `/owner/repo/actions/runs/{run_id}/jobs/6` which gives the run_id and job index for log access.

---

## 13. Empty vs Null Fields

In Forgejo API responses:
- `null` means the field has no value (e.g., `"merged_by": null` means not merged)
- Empty string `""` means the field exists but is empty
- Missing field means it wasn't requested or doesn't apply
- `"mergeable": null` specifically means merge status hasn't been computed yet (retry later)
