This is the full developer documentation for unfault
# Welcome to unfault
> Learn how to use Unfault to build production-ready code.
Production instincts, baked in.
Unfault is not a linter in the usual sense as it focuses on production-readiness issues that would only surface in real-world usage.
At Unfault, we believe production lives in the code that we all write every day. Our code goes through testing, thorough code and performance reviews and from time to time we all have to deal with the dreaded production bug.
Unfault does not replace testing or code reviews, but rather complements them. We don’t make promises about catching all bugs, but we want to help you start treating these facets of writing code as first-class citizens.
**So how do we do that we hear you asking?**
Well, Unfault reviews your code for patterns that tend to cause problems once systems run for real, things like unbounded calls, silent failure paths, or missing timeouts.
Also, because we don’t want to be too prescriptive, we don’t enforce any of these patterns, but rather suggest them as a way to improve your code.
The cherry on the cake is that we try to be as fast and out of your way as possible. We are developers first, so we tried to build Unfault so it doesn’t piss us off first.
## Quick Links
[Section titled “Quick Links”](#quick-links)
[Getting Started ](/docs/getting-started/)Learn the basics of Unfault and what it does.
[CLI Reference ](/docs/guides/cli/)Complete guide to the unfault command-line interface.
[Rules Catalog ](/docs/reference/rules/)All 41 production-readiness patterns we detect.
[CI/CD Integration ](/docs/guides/cicd/)Add Unfault to your build pipeline.
## Use it wherever you code
[Section titled “Use it wherever you code”](#use-it-wherever-you-code)
unfault integrates into your existing workflow:
* **Editor**: Use the VS Code extension for real-time feedback as you type
* **Terminal**: Run `unfault check .` before you push
* **CI/CD**: Add unfault to your pipeline for automated checks
* **AI Agents**: Agents can call the CLI directly via MCP server
# Use Unfault with AI-agents
> Complete guide to using the Unfault command-line interface.
Unfault works well with AI coding assistants like Claude or KiloCode. Add this to your [AGENTS.md](https://agents.md/) or system prompt:
```markdown
# Code Production-Readiness
- Run `unfault review` before committing to check for production issues.
- Use `unfault review --output full` to see suggested patches.
```
The CLI returns structured output that AI agents can parse and act on.
Tip
You can feed this documentation to your AI agent as context via the two [llmstxt](https://llmstxt.org/) documents:
* [llms.txt](https://unfault.dev/llms.txt)
* [llms-full.txt](https://unfault.dev/llms-full.txt)
# CI/CD Integration
> Adding Unfault to your continuous integration pipeline.
Unfault integrates with GitHub Actions, GitLab CI, and other CI/CD systems to catch issues before they reach production.
## Output Formats
[Section titled “Output Formats”](#output-formats)
The `unfault review` command supports multiple output formats:
| Format | Flag | Use Case |
| ------- | ---------------- | --------------------------------------------- |
| `text` | `--output=text` | Human-readable terminal output (default) |
| `json` | `--output=json` | Machine-readable JSON for custom integrations |
| `sarif` | `--output=sarif` | SARIF format for GitHub Code Scanning |
Tip
Use `--output=sarif` for seamless integration with GitHub’s security features, including inline annotations in pull requests and the Security tab.
## GitHub Actions
[Section titled “GitHub Actions”](#github-actions)
Add this workflow to `.github/workflows/unfault.yml`:
```yaml
name: Unfault
on:
pull_request:
push:
branches: [main]
jobs:
unfault:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Unfault
run: curl -sSL https://unfault.dev/get | bash
- name: Run Unfault review
env:
UNFAULT_API_KEY: ${{ secrets.UNFAULT_API_KEY }}
run: unfault review --output sarif > results.sarif
- name: Upload SARIF to GitHub
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
```
This configuration:
1. Runs on every pull request and push to main
2. Uploads SARIF results to GitHub’s Security tab
3. Shows findings inline in pull request diffs with full context
### Getting an API Key
[Section titled “Getting an API Key”](#getting-an-api-key)
1. Go to your [Unfault dashboard](https://app.unfault.dev)
2. Navigate to Settings > API Keys
3. Create a new key with appropriate permissions
4. Add it to your repository secrets as `UNFAULT_API_KEY`
## CI Platforms
[Section titled “CI Platforms”](#ci-platforms)
* GitLab CI
Add to `.gitlab-ci.yml`:
```yaml
unfault:
image: debian:bookworm-slim
stage: test
before_script:
- apt-get update && apt-get install -y curl
- curl -sSL https://unfault.dev/get | bash
script:
- unfault review --output sarif > gl-code-quality-report.json
artifacts:
reports:
sast: gl-code-quality-report.json
variables:
UNFAULT_API_KEY: $UNFAULT_API_KEY
```
Note
GitLab supports SARIF format for its [SAST reports](https://docs.gitlab.com/ee/user/application_security/sast/). Results will appear in merge request security widgets.
* CircleCI
Add to `.circleci/config.yml`:
```yaml
version: 2.1
jobs:
unfault:
docker:
- image: cimg/base:current
steps:
- checkout
- run:
name: Install Unfault
command: curl -sSL https://unfault.dev/get | bash
- run:
name: Run Unfault
command: unfault review --output sarif > results.sarif
- store_artifacts:
path: results.sarif
workflows:
check:
jobs:
- unfault
```
## Exit Codes
[Section titled “Exit Codes”](#exit-codes)
The CLI uses standard exit codes for CI/CD integration:
| Code | Meaning | Action |
| ---- | --------------------- | ------------------- |
| `0` | Success, no issues | ✅ Proceed |
| `1` | General error | 🔍 Check logs |
| `2` | Configuration error | Run `unfault login` |
| `3` | Authentication failed | Re-authenticate |
| `4` | Network error | Check connectivity |
| `5` | **Findings detected** | 🚨 Review issues |
| `6` | Invalid input | Check arguments |
| `7` | Service unavailable | Retry later |
| `8` | Session error | Retry analysis |
| `10` | Subscription required | Upgrade plan |
## Pull Request Comments
[Section titled “Pull Request Comments”](#pull-request-comments)
Caution
When using SARIF upload (recommended), GitHub automatically shows findings as annotations in pull requests. The manual approach below is only needed for custom comment formatting.
For custom PR comments with JSON output:
```yaml
- name: Run Unfault review
id: unfault
run: |
unfault review --output json > results.json
echo "findings=$(cat results.json | jq '.findings | length')" >> $GITHUB_OUTPUT
- name: Comment on PR
if: ${{ steps.unfault.outputs.findings > 0 }}
uses: actions/github-script@v7
with:
script: |
const results = require('./results.json');
const body = `## Unfault found ${results.findings.length} findings\n\n` +
results.findings.map(f => `- **${f.rule}**: ${f.message}`).join('\n');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
```
## Caching
[Section titled “Caching”](#caching)
Speed up CI runs by caching the Unfault binary:
```yaml
- name: Cache Unfault
uses: actions/cache@v4
with:
path: ~/.local/bin/unfault
key: unfault-${{ runner.os }}
- name: Install Unfault
if: steps.cache.outputs.cache-hit != 'true'
run: curl -sSL https://unfault.dev/get | bash
```
## Environment Variables
[Section titled “Environment Variables”](#environment-variables)
| Variable | Purpose |
| ------------------ | ---------------------- |
| `UNFAULT_API_KEY` | Authentication for CI |
| `UNFAULT_NO_COLOR` | Disable colored output |
| `UNFAULT_DEBUG` | Enable debug logging |
# CLI Usage
> Complete guide to using the Unfault command-line interface.
The Unfault CLI is the primary interface for analyzing your codebase and getting production-readiness feedback.
## Installation
[Section titled “Installation”](#installation)
The Unfault CLI is available for macOS, Linux, and Windows as an executable binary. You can install it using one of the following methods.
* From releases
Download the right release for your platform from [GitHub Releases](https://github.com/unfault/cli/releases) and add it to your PATH.
```bash
curl -sSL https://unfault.dev/get | bash
```
* From crates.io
```bash
cargo install unfault
```
* From source
```bash
git clone https://github.com/unfault/cli
cd cli
cargo build --release
```
## Commands
[Section titled “Commands”](#commands)
The unfault cli has a set of commands that you can use to interact with the Unfault platform.
### unfault login
[Section titled “unfault login”](#unfault-login)
Authenticate using secure device flow. No API keys in your terminal history.
```bash
unfault login
# Visit https://app.unfault.dev/auth/device/ and enter the displayed code
```
Note
Your authentication token is stored securely and used for all subsequent commands. See the [Configuration](#configuration) section for file locations on each platform.
### unfault review
[Section titled “unfault review”](#unfault-review)
Analyze your codebase for production-readiness issues.
```bash
# Basic output (grouped by severity)
unfault review
# Full details with suggested fixes
unfault review --output full
# JSON for integration with other tools
unfault review --output json
# Focus on specific dimensions
unfault review --dimension stability --dimension performance
```
**Output Modes:**
| Mode | Description |
| --------- | ------------------------------------------ |
| `basic` | Grouped by severity, rule counts (default) |
| `concise` | Summary statistics only |
| `full` | Detailed findings with diffs |
| `json` | Machine-readable output |
### unfault ask
[Section titled “unfault ask”](#unfault-ask)
Query your project’s health using natural language.
```bash
# Ask about your codebase
unfault ask "What are my main stability concerns?"
# Scope to a specific workspace
unfault ask "Show recent issues" --workspace wks_abc123
# Get raw context without AI synthesis
unfault ask "Performance problems" --no-llm
```
Note
The `ask` command requires you to have run `unfault review` at least once.
Configure an LLM for AI-powered answers:
* OpenAI
```bash
unfault config llm openai --model gpt-5.1
```
* Anthropic
```bash
unfault config llm anthropic --model claude-sonnet-4-5
```
* Ollama
```bash
unfault config llm ollama --model llama3.2
```
Tip
When asking questions, you can use the `--no-llm` flag to get raw context without AI synthesis.
### unfault status
[Section titled “unfault status”](#unfault-status)
Check authentication and connectivity.
```bash
unfault status
```
### unfault config
[Section titled “unfault config”](#unfault-config)
Manage CLI configuration.
```bash
# Show current config
unfault config show
# Configure LLM provider
unfault config llm openai --model gpt-4o
# View LLM settings
unfault config llm show
# Remove LLM configuration
unfault config llm remove
```
## Exit Codes
[Section titled “Exit Codes”](#exit-codes)
The CLI uses standard exit codes for CI/CD integration:
| Code | Meaning | Action |
| ---- | --------------------- | ------------------- |
| `0` | Success, no issues | ✅ Proceed |
| `1` | General error | 🔍 Check logs |
| `2` | Configuration error | Run `unfault login` |
| `3` | Authentication failed | Re-authenticate |
| `4` | Network error | Check connectivity |
| `5` | **Findings detected** | 🚨 Review issues |
| `6` | Invalid input | Check arguments |
| `7` | Service unavailable | Retry later |
| `8` | Session error | Retry analysis |
| `10` | Subscription required | Upgrade plan |
## Configuration
[Section titled “Configuration”](#configuration)
The CLI configuration file location depends on your operating system:
| Platform | Location |
| ----------- | ------------------------------------------------------------------------- |
| **Linux** | `~/.config/unfault/config.json` or `$XDG_CONFIG_HOME/unfault/config.json` |
| **macOS** | `~/.config/unfault/config.json` or `$XDG_CONFIG_HOME/unfault/config.json` |
| **Windows** | `%USERPROFILE%\.config\unfault\config.json` |
Note
All platforms use the same `.config` directory pattern for consistency, rather than platform-specific locations like `~/Library/Application Support` (macOS) or `%APPDATA%` (Windows).
Example configuration:
```json
{
"api_key": "uf_live_...",
"base_url": "https://api.unfault.dev",
"llm": {
"provider": "openai",
"model": "gpt-4",
"api_key": "sk-..."
}
}
```
## Environment Variables
[Section titled “Environment Variables”](#environment-variables)
| Variable | Description |
| ------------------- | ------------------------------------- |
| `UNFAULT_BASE_URL` | Override API endpoint |
| `OPENAI_API_KEY` | OpenAI API key (for `ask` command) |
| `ANTHROPIC_API_KEY` | Anthropic API key (for `ask` command) |
## Using with AI Agents
[Section titled “Using with AI Agents”](#using-with-ai-agents)
Tip
Unfault works well with AI coding assistants like Claude or ChatGPT. Add this to your `AGENTS.md` or system prompt:
```markdown
# Code Quality
Run `unfault review` before committing to check for production issues.
Use `unfault review --output full` to see suggested patches.
```
The CLI returns structured output that AI agents can parse and act on.
# VS Code Extension
> Using Unfault in Visual Studio Code for real-time feedback.
The Unfault [VS Code extension](https://marketplace.visualstudio.com/items?itemName=unfault.unfault) VS Code extension provides real-time feedback as you write code.
## Installation
[Section titled “Installation”](#installation)
1. Open VS Code
2. Press `Cmd+Shift+X` (macOS) or `Ctrl+Shift+X` (Windows/Linux)
3. Search for “Unfault”
4. Click Install
## Authentication
[Section titled “Authentication”](#authentication)
On first use, the extension prompts you to sign in. Click the Unfault icon in the status bar, then “Sign In”.
Note
If you’ve already authenticated via CLI, the extension will use those credentials automatically.
## Features
[Section titled “Features”](#features)
### Real-time Diagnostics
[Section titled “Real-time Diagnostics”](#real-time-diagnostics)
As you edit code, Unfault highlights issues inline with squiggly underlines:
* **Red** - Critical issues that will likely cause production problems
* **Yellow** - Warnings worth addressing
* **Blue** - Informational suggestions
Hover over any highlight to see the full explanation.
### Quick Fixes
[Section titled “Quick Fixes”](#quick-fixes)
When Unfault detects an issue, click the lightbulb icon or press `Cmd+.` to see available fixes.
Select a fix to apply it immediately, or choose “Show Details” to learn more.
### Problems Panel
[Section titled “Problems Panel”](#problems-panel)
All findings appear in VS Code’s Problems panel (`View > Problems`).
Click any entry to jump to the relevant code location.
### Scan on Save
[Section titled “Scan on Save”](#scan-on-save)
By default, Unfault scans your file every time you save. You can change this in settings.
## Configuration
[Section titled “Configuration”](#configuration)
Open Settings (`Cmd+,`) and search for “Unfault”:
| Setting | Description | Default |
| ------------------------- | ------------------------ | --------- |
| `unfault.scanOnSave` | Scan files when saved | `true` |
| `unfault.scanOnOpen` | Scan files when opened | `true` |
| `unfault.showInlineHints` | Show inline hints | `true` |
| `unfault.profiles` | Active profiles | `["all"]` |
| `unfault.severity` | Minimum severity to show | `warning` |
### Workspace Configuration
[Section titled “Workspace Configuration”](#workspace-configuration)
Configure Unfault in your project’s manifest file (`pyproject.toml`, `Cargo.toml`, `package.json`) or use a standalone `unfault.toml`. See the [Configuration Reference](/docs/reference/configuration) for details.
## Commands
[Section titled “Commands”](#commands)
Access commands via Command Palette (`Cmd+Shift+P`):
| Command | Description |
| ------------------------------ | ------------------------------------- |
| **Unfault: Scan Current File** | Run analysis on active file |
| **Unfault: Scan Workspace** | Analyze entire workspace |
| **Unfault: Apply All Fixes** | Apply all available fixes |
| **Unfault: Show Rule Details** | View documentation for selected issue |
| **Unfault: Sign In** | Authenticate with Unfault |
| **Unfault: Sign Out** | Remove credentials |
## Status Bar
[Section titled “Status Bar”](#status-bar)
The status bar shows:
* **Unfault icon** - Click to run a scan
* **Issue count** - Number of issues in current file
* **Auth status** - Connection state
## Keyboard Shortcuts
[Section titled “Keyboard Shortcuts”](#keyboard-shortcuts)
| Shortcut | Action |
| ------------- | ---------------------- |
| `Cmd+Shift+U` | Scan current file |
| `Cmd+.` | Show quick fixes |
| `F8` | Go to next problem |
| `Shift+F8` | Go to previous problem |
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
### Extension not loading
[Section titled “Extension not loading”](#extension-not-loading)
1. Check that you have a valid subscription
2. Run “Unfault: Sign In” from Command Palette
3. Check Output panel for error messages
### Scans are slow
[Section titled “Scans are slow”](#scans-are-slow)
The extension analyzes files on save. For large projects, consider:
* Closing files you’re not actively editing
* Using VS Code’s built-in file exclusions in Settings → Files: Exclude
### Issues not matching CLI
[Section titled “Issues not matching CLI”](#issues-not-matching-cli)
Ensure VS Code and CLI are using the same configuration file.
# Installation
> How to install and set up Unfault in your development environment.
Unfault can be installed as a CLI tool or as a VS Code extension. Choose the method that fits your workflow.
First things first! 👋
Before installing the CLI or VS Code extension, you’ll need an Unfault account. Don’t have one yet? No worries — [sign up for free](https://app.unfault.dev/sign-up) in just a few seconds, and you’ll be ready to go!
## Quick Install
[Section titled “Quick Install”](#quick-install)
The fastest way to install the Unfault CLI:
* macOS / Linux
```bash
curl -sSL https://unfault.dev/get | bash
```
* Windows (PowerShell)
```powershell
irm https://unfault.dev/get.ps1 | iex
```
This downloads the [latest release from GitHub](https://github.com/unfault/cli/releases/latest) and installs it to your system.
### Authentication
[Section titled “Authentication”](#authentication)
1. Authenticate with your Unfault account:
```bash
unfault login
```
This will output the following:
```plaintext
Initiating device authentication flow...
Visit /auth/device and enter code: VZ8-VK3
Waiting for authentication...
```
Copy the code and visit to complete the login.
2. Verify your installation:
```bash
unfault status
```
This should output the following:
```plaintext
Unfault CLI Status
────────────────────────────────────────
✓ Configuration: Found
ℹ API Endpoint: https://app.unfault.dev
✓ API Status: Healthy
ℹ API Key: sk_live_abc1...**
✓ Authentication: Configured
✓ Ready to analyze code. Run `unfault review` to start.
```
Once complete, your credentials are stored in `~/.unfault/config.toml`.
Danger
The API key is stored in plaintext, so ensure your system is secure.
Tip
If you’re using a CI/CD system, you can authenticate with the [`UNFAULT_API_KEY` environment variable](/docs/guides/cicd).
## VS Code Extension
[Section titled “VS Code Extension”](#vs-code-extension)
1. Open VS Code
2. Go to Extensions (`Cmd+Shift+X` / `Ctrl+Shift+X`)
3. Search for “Unfault”
4. Click Install
The extension will prompt you to sign in on first use.
## Next Steps
[Section titled “Next Steps”](#next-steps)
Quick Start
Run your first scan and understand the output. [Get started →](/docs/quick-start)
CLI Reference
Complete guide to command-line options. [View CLI docs →](/docs/guides/cli)
# Quick Start
> Run your first Unfault review in 30 seconds.
This guide walks you through running your first review and understanding the results.
## Run Your First Review
[Section titled “Run Your First Review”](#run-your-first-review)
Navigate to your project directory and run:
```bash
unfault review
```
Unfault will analyze your codebase and display findings in your terminal.
## Understanding Output
[Section titled “Understanding Output”](#understanding-output)
A typical review produces output like this:
```plaintext
→ Analyzing rag-app...
Languages: python
Frameworks: fastapi
Dimensions: stability, correctness, performance
Found 1 matching source files
Reviewed in 282ms (trace: 13260fc6)
⚠ Found 11 issues
🟠 High (3 issues)
[python.db.missing_timeout] Database create_engine call without timeout (1x)
[python.http.blocking_in_async] Blocking HTTP call via `requests.get` inside async function `fetch_external_data` (1x)
[python.resilience.missing_circuit_breaker] HTTP call to external service in `fetch_external_data` lacks circuit breaker protection (1x)
🟡 Medium (7 issues)
[fastapi.missing_cors] FastAPI app `app` has no CORS middleware configured (1x)
[python.fastapi.missing_exception_handler] FastAPI app `app` has no exception handlers (1x)
[python.fastapi.missing_request_timeout] FastAPI app `app` has no request timeout middleware (1x)
[python.http.missing_retry] HTTP call via `requests`.get has no retry policy (1x)
[python.http.missing_timeout] HTTP call via `requests`.get has no timeout (1x)
[python.missing_correlation_id] FastAPI app 'app' missing correlation ID middleware (1x)
[python.sqlalchemy.pgvector_suboptimal_query] pgvector: Missing LIMIT on vector similarity query (1x)
🔵 Low (1 issue)
[python.sqlalchemy.pgvector_suboptimal_query] pgvector: Use inner product (<#>) instead of cosine (<=>) (1x)
```
Note the color coding:
* 🟠 High: Issues that could cause production failuresand should be attended to first
* 🟡 Medium: Important but not immediately critical
* 🔵 Low: Minor improvements
## Viewing Suggested Fixes
[Section titled “Viewing Suggested Fixes”](#viewing-suggested-fixes)
To see the full details with patches:
```bash
unfault review --output full
```
This shows the suggested code change for each issue.
## Output Formats
[Section titled “Output Formats”](#output-formats)
Choose the format that fits your workflow:
* Default
```bash
# Grouped by severity
unfault review
```
* Concise
```bash
# Summary statistics only
unfault review --output concise
```
* Full
```bash
# Full details with diffs
unfault review --output full
```
* JSON
```bash
# Machine-readable for CI/CD
unfault review --output json
```
* SARIF
```bash
# Code Scanning ready
unfault review --output sarif
```
## Filtering by Dimension
[Section titled “Filtering by Dimension”](#filtering-by-dimension)
Focus on specific types of issues:
```bash
# Only stability issues
unfault review --dimension stability
# Only performance issues
unfault review --dimension performance
# Multiple dimensions
unfault review --dimension stability --dimension correctness
```
## CI/CD Integration
[Section titled “CI/CD Integration”](#cicd-integration)
Use exit codes to gate deployments:
```bash
unfault review
if [ $? -eq 5 ]; then
echo "Production readiness issues found. Blocking deployment."
exit 1
fi
```
Tip
Exit code `5` means findings were detected. See the [CLI reference](/docs/guides/cli#exit-codes) for all exit codes.
## Next Steps
[Section titled “Next Steps”](#next-steps)
[CLI Commands ](/docs/guides/cli)Learn about all CLI commands in detail.
[VS Code Integration ](/docs/guides/vscode)Get real-time feedback as you code.
[CI/CD Pipeline ](/docs/guides/cicd)Add Unfault to your build process.
# Configuration
> Configuration options for Unfault CLI and workspaces.
Unfault can be configured at the user level and per-workspace.
## User Configuration
[Section titled “User Configuration”](#user-configuration)
The configuration file location depends on your operating system:
| Platform | Location |
| ----------- | ------------------------------------------------------------------------- |
| **Linux** | `~/.config/unfault/config.json` or `$XDG_CONFIG_HOME/unfault/config.json` |
| **macOS** | `~/.config/unfault/config.json` or `$XDG_CONFIG_HOME/unfault/config.json` |
| **Windows** | `%USERPROFILE%\.config\unfault\config.json` |
Note
All platforms use the same `.config` directory pattern for consistency, rather than platform-specific locations like `~/Library/Application Support` (macOS) or `%APPDATA%` (Windows).
Example configuration:
```json
{
"api_key": "uf_live_...",
"base_url": "https://api.unfault.dev",
"llm": {
"provider": "openai",
"model": "gpt-4",
"api_key": "sk-..."
}
}
```
### Authentication
[Section titled “Authentication”](#authentication)
After running `unfault login`, your API key is stored automatically.
For CI/CD environments, set the API key via environment variable:
```bash
export UNFAULT_API_KEY="sk_live_..."
```
### LLM Configuration
[Section titled “LLM Configuration”](#llm-configuration)
For the `unfault ask` command, configure an LLM provider:
* OpenAI
```bash
unfault config llm openai --model gpt-5.1
```
* Anthropic
```bash
unfault config llm anthropic --model claude-4-5-sonnet-latest
```
* Ollama
```bash
unfault config llm ollama --model llama3.2
```
## Workspace Configuration
[Section titled “Workspace Configuration”](#workspace-configuration)
Unfault reads configuration from your project’s manifest file, avoiding the need for a dedicated config file. The configuration location depends on your language:
* Python (pyproject.toml)
```toml
[tool.unfault]
# Override auto-detected profile
profile = "python_fastapi_backend"
# Limit analysis to specific dimensions
dimensions = ["stability", "correctness", "performance"]
[tool.unfault.rules]
# Rules to exclude (supports glob patterns)
exclude = [
"python.missing_structured_logging", # We use custom logging
"python.http.*", # All HTTP rules
]
# Additional rules to include
include = ["python.security.*"]
# Severity overrides
[tool.unfault.rules.severity]
"python.bare_except" = "low"
```
* Rust (Cargo.toml)
```toml
[package.metadata.unfault]
profile = "rust_axum_service"
dimensions = ["stability", "correctness"]
[package.metadata.unfault.rules]
exclude = ["rust.println_in_lib"]
[package.metadata.unfault.rules.severity]
"rust.unsafe_unwrap" = "critical"
```
* JavaScript/TypeScript (package.json)
```json
{
"name": "my-app",
"unfault": {
"profile": "typescript_express_backend",
"dimensions": ["stability", "security"],
"rules": {
"exclude": ["typescript.console_in_production"],
"include": ["typescript.security.*"],
"severity": {
"typescript.empty_catch": "critical"
}
}
}
}
```
* Standalone (.unfault.toml)
```toml
# For Go projects, multi-language repos, or any project
# where you prefer a dedicated config file
# Override auto-detected profile (optional)
profile = "go_gin_service"
# Limit analysis to specific dimensions (optional)
# Available: stability, correctness, performance, scalability, security, maintainability
dimensions = ["stability", "performance"]
[rules]
# Rules to exclude - supports exact IDs and glob patterns
exclude = [
"go.missing_structured_logging",
"go.http.*", # All HTTP-related rules
]
# Additional rules to include beyond profile defaults
include = ["go.security.*"]
# Severity overrides: low, medium, high, critical
[rules.severity]
"go.unchecked_error" = "critical"
"go.defer_in_loop" = "low"
```
Tip
Use `.unfault.toml` when your project doesn’t have a `pyproject.toml`, `Cargo.toml`, or `package.json`, or when you prefer to keep Unfault configuration separate from your package manifest.
### Configuration Priority
[Section titled “Configuration Priority”](#configuration-priority)
When multiple configuration sources exist:
1. **Manifest files are checked first** — `pyproject.toml`, `Cargo.toml`, then `package.json`
2. **`.unfault.toml` is the fallback** — Used when no manifest contains unfault configuration
3. **No merging** — Only one source is used per project (the first one found with unfault config)
### Rule Patterns
[Section titled “Rule Patterns”](#rule-patterns)
Patterns for `exclude` and `include` use glob syntax:
| Pattern | Matches | Does Not Match |
| ----------------------------- | ---------------------------------------------------------- | ----------------------------- |
| `python.http.missing_timeout` | Exact match only | Any other rule |
| `python.http.*` | `python.http.missing_timeout`, `python.http.missing_retry` | `python.http.client.timeout` |
| `*.missing_timeout` | `python.missing_timeout`, `go.missing_timeout` | `python.http.missing_timeout` |
| `python.**` | All rules starting with `python.` | Rules from other languages |
### Disabling Rules
[Section titled “Disabling Rules”](#disabling-rules)
To disable a rule project-wide, add it to the `exclude` list in your config.
You can also disable rules inline in your code:
* Go
```go
// unfault:disable http_client_missing_timeout
client := http.Client{}
```
* Python
```python
# unfault:disable-next-line n_plus_one_query
for user in users:
orders = get_orders(user.id)
```
## Environment Variables
[Section titled “Environment Variables”](#environment-variables)
| Variable | Description |
| ------------------- | ----------------------------------- |
| `UNFAULT_API_KEY` | API key for authentication |
| `UNFAULT_BASE_URL` | Override API endpoint (enterprise) |
| `OPENAI_API_KEY` | OpenAI API key for `ask` command |
| `ANTHROPIC_API_KEY` | Anthropic API key for `ask` command |
## Configuration Precedence
[Section titled “Configuration Precedence”](#configuration-precedence)
Settings are applied in this order (later overrides earlier):
1. Default values
2. User config (see [User Configuration](#user-configuration) for platform-specific paths)
3. Workspace config (`pyproject.toml`, `Cargo.toml`, `package.json`, or `.unfault.toml`)
4. Environment variables
5. Command-line flags
# Rules Reference
> Complete reference for all Unfault detection rules organized by language.
Unfault analyzes your code across **195 production-readiness rules** in Python, Go, Rust, and TypeScript. Each rule targets patterns that cause real incidents in production systems.
## Rules by Language
[Section titled “Rules by Language”](#rules-by-language)
[Python Rules ](/docs/reference/rules/python/)60 rules covering stability, correctness, performance, and more.
[Go Rules ](/docs/reference/rules/go/)56 rules for goroutine safety, error handling, and production patterns.
[Rust Rules ](/docs/reference/rules/rust/)44 rules for memory safety, async correctness, and panic prevention.
[TypeScript Rules ](/docs/reference/rules/typescript/)35 rules for promise handling, type safety, and Node.js patterns.
## Rules by Dimension
[Section titled “Rules by Dimension”](#rules-by-dimension)
Unfault organizes rules into seven dimensions that map to the qualities that keep systems running reliably:
| Dimension | Focus | Example Rules |
| ------------------- | ------------------------------------------ | ------------------------------------------------- |
| **Stability** | Preventing crashes and service degradation | Timeouts, graceful shutdown, bounded retries |
| **Correctness** | Preventing bugs and data corruption | SQL injection, error handling, type safety |
| **Performance** | Preventing slowdowns | N+1 queries, blocking in async, CPU in event loop |
| **Scalability** | Ensuring systems handle growth | Bounded concurrency, resource limits |
| **Observability** | Improving monitoring and debugging | Structured logging, correlation IDs, tracing |
| **Security** | Preventing vulnerabilities | Hardcoded secrets, unsafe eval, input validation |
| **Maintainability** | Ensuring code quality | Halstead complexity, code duplication |
## Severity Levels
[Section titled “Severity Levels”](#severity-levels)
Each rule is assigned a severity based on its potential impact:
* Critical — Security vulnerabilities or data corruption risks
* High — Can cause outages or significant bugs
* Medium — May cause issues under load or edge cases
* Low — Best practices and code quality
## Auto-Fix Support
[Section titled “Auto-Fix Support”](#auto-fix-support)
Most Unfault rules include auto-fix patches. When Unfault detects a violation, it can generate a diff showing exactly how to fix the issue. Apply patches with:
```bash
unfault review --apply
```
# Go Rules
> All Unfault detection rules for Go code.
Unfault includes **55 rules** for Go, covering core language patterns, goroutine safety, and popular frameworks like Gin, GORM, Echo, gRPC, and Redis.
## Core Rules (43 rules)
[Section titled “Core Rules (43 rules)”](#core-rules-43-rules)
| Rule | Dimension | Severity |
| ------------------------------------------------------------------------------------ | --------------- | -------- |
| [unchecked\_error](/docs/reference/rules/go/unchecked-error/) | Correctness | Medium |
| [defer\_in\_loop](/docs/reference/rules/go/defer-in-loop/) | Performance | Medium |
| [goroutine\_leak](/docs/reference/rules/go/goroutine-leak/) | Stability | High |
| [sql\_injection](/docs/reference/rules/go/sql-injection/) | Security | Critical |
| [http\_missing\_timeout](/docs/reference/rules/go/http-missing-timeout/) | Stability | High |
| [missing\_structured\_logging](/docs/reference/rules/go/missing-structured-logging/) | Observability | Low |
| [unbounded\_goroutines](/docs/reference/rules/go/unbounded-goroutines/) | Scalability | High |
| [race\_condition](/docs/reference/rules/go/race-condition/) | Correctness | High |
| [hardcoded\_secrets](/docs/reference/rules/go/hardcoded-secrets/) | Security | Critical |
| [type\_assertion\_no\_ok](/docs/reference/rules/go/type-assertion-no-ok/) | Stability | Medium |
| [context\_background](/docs/reference/rules/go/context-background/) | Stability | Medium |
| [halstead\_complexity](/docs/reference/rules/go/halstead-complexity/) | Maintainability | Low |
| [bare\_recover](/docs/reference/rules/go/bare-recover/) | Stability | Medium |
| [channel\_never\_closed](/docs/reference/rules/go/channel-never-closed/) | Stability | High |
| [circuit\_breaker](/docs/reference/rules/go/circuit-breaker/) | Stability | Medium |
| [concurrent\_map\_access](/docs/reference/rules/go/concurrent-map-access/) | Correctness | High |
| [cpu\_in\_hot\_path](/docs/reference/rules/go/cpu-in-hot-path/) | Performance | Medium |
| [empty\_critical\_section](/docs/reference/rules/go/empty-critical-section/) | Correctness | Medium |
| [ephemeral\_filesystem\_write](/docs/reference/rules/go/ephemeral-filesystem-write/) | Stability | Medium |
| [error\_type\_assertion](/docs/reference/rules/go/error-type-assertion/) | Correctness | Medium |
| [global\_mutable\_state](/docs/reference/rules/go/global-mutable-state/) | Correctness | High |
| [graceful\_shutdown](/docs/reference/rules/go/graceful-shutdown/) | Stability | High |
| [http\_retry](/docs/reference/rules/go/http-retry/) | Stability | Medium |
| [idempotency\_key](/docs/reference/rules/go/idempotency-key/) | Correctness | Medium |
| [large\_response\_memory](/docs/reference/rules/go/large-response-memory/) | Scalability | High |
| [map\_without\_size\_hint](/docs/reference/rules/go/map-without-size-hint/) | Performance | Low |
| [missing\_correlation\_id](/docs/reference/rules/go/missing-correlation-id/) | Observability | Medium |
| [missing\_tracing](/docs/reference/rules/go/missing-tracing/) | Observability | Low |
| [panic\_in\_library](/docs/reference/rules/go/panic-in-library/) | Stability | High |
| [rate\_limiting](/docs/reference/rules/go/rate-limiting/) | Scalability | Medium |
| [reflect\_in\_hot\_path](/docs/reference/rules/go/reflect-in-hot-path/) | Performance | Medium |
| [regex\_compile](/docs/reference/rules/go/regex-compile/) | Performance | Low |
| [sentinel\_error\_comparison](/docs/reference/rules/go/sentinel-error-comparison/) | Correctness | Medium |
| [slice\_append\_in\_loop](/docs/reference/rules/go/slice-append-in-loop/) | Performance | Medium |
| [slice\_memory\_leak](/docs/reference/rules/go/slice-memory-leak/) | Stability | High |
| [sync\_dns\_lookup](/docs/reference/rules/go/sync-dns-lookup/) | Performance | Medium |
| [transaction\_boundary](/docs/reference/rules/go/transaction-boundary/) | Correctness | High |
| [unbounded\_cache](/docs/reference/rules/go/unbounded-cache/) | Scalability | High |
| [unbounded\_memory](/docs/reference/rules/go/unbounded-memory/) | Scalability | High |
| [unbounded\_retry](/docs/reference/rules/go/unbounded-retry/) | Stability | High |
| [uncancelled\_context](/docs/reference/rules/go/uncancelled-context/) | Stability | Medium |
| [unhandled\_error\_goroutine](/docs/reference/rules/go/unhandled-error-goroutine/) | Stability | High |
| [unsafe\_template](/docs/reference/rules/go/unsafe-template/) | Security | High |
## Framework Rules
[Section titled “Framework Rules”](#framework-rules)
### Gin (2 rules)
[Section titled “Gin (2 rules)”](#gin-2-rules)
| Rule | Dimension | Severity |
| ----------------------------------------------------------------------- | ----------- | -------- |
| [missing\_validation](/docs/reference/rules/go/gin-missing-validation/) | Correctness | Medium |
| [request\_validation](/docs/reference/rules/go/gin-request-validation/) | Correctness | High |
### Echo (2 rules)
[Section titled “Echo (2 rules)”](#echo-2-rules)
| Rule | Dimension | Severity |
| ------------------------------------------------------------------------ | ----------- | -------- |
| [request\_validation](/docs/reference/rules/go/echo-request-validation/) | Correctness | High |
| [missing\_middleware](/docs/reference/rules/go/echo-missing-middleware/) | Stability | Medium |
### GORM (4 rules)
[Section titled “GORM (4 rules)”](#gorm-4-rules)
| Rule | Dimension | Severity |
| ------------------------------------------------------------------------ | ----------- | -------- |
| [n\_plus\_one](/docs/reference/rules/go/gorm-n-plus-one/) | Performance | High |
| [session\_management](/docs/reference/rules/go/gorm-session-management/) | Stability | High |
| [connection\_pool](/docs/reference/rules/go/gorm-connection-pool/) | Scalability | High |
| [query\_timeout](/docs/reference/rules/go/gorm-query-timeout/) | Stability | High |
### gRPC (1 rule)
[Section titled “gRPC (1 rule)”](#grpc-1-rule)
| Rule | Dimension | Severity |
| -------------------------------------------------------------------- | --------- | -------- |
| [missing\_deadline](/docs/reference/rules/go/grpc-missing-deadline/) | Stability | High |
### Redis (2 rules)
[Section titled “Redis (2 rules)”](#redis-2-rules)
| Rule | Dimension | Severity |
| ------------------------------------------------------------------- | ----------- | -------- |
| [missing\_ttl](/docs/reference/rules/go/redis-missing-ttl/) | Scalability | High |
| [connection\_pool](/docs/reference/rules/go/redis-connection-pool/) | Scalability | High |
### net/http (1 rule)
[Section titled “net/http (1 rule)”](#nethttp-1-rule)
| Rule | Dimension | Severity |
| --------------------------------------------------------------------- | --------- | -------- |
| [missing\_timeout](/docs/reference/rules/go/nethttp-missing-timeout/) | Stability | Critical |
# go.bare_recover
> Detects bare recover() calls that swallow all panics without logging.
Correctness High
Detects `recover()` calls that catch panics without logging or re-panicking, silently hiding errors.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Bare recover calls:
* **Hide bugs** — Panics indicate serious problems that need attention
* **Lose context** — No logs means no way to debug issues
* **Mask failures** — Operations silently fail without notification
* **Delay fixes** — Problems go unnoticed until they cascade
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (swallows panic)
func handler() {
defer func() {
recover() // Panic is silently ignored
}()
riskyOperation()
}
```
```go
// ✅ After (logs and handles)
func handler() {
defer func() {
if r := recover(); r != nil {
log.Error("panic recovered",
"error", r,
"stack", string(debug.Stack()))
// Optionally re-panic or return error
}
}()
riskyOperation()
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `recover()` without checking return value
* `recover()` without logging
* Naked `defer recover()` patterns
* Silent panic suppression
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates patches that add proper panic handling:
```go
defer func() {
if r := recover(); r != nil {
// Log with stack trace
log.Error("panic recovered",
"error", r,
"stack", string(debug.Stack()))
// Convert to error if possible
err = fmt.Errorf("panic: %v", r)
}
}()
```
Caution
Recovering from panics should be rare. Most panics indicate bugs that should be fixed, not suppressed. Use recover only at API boundaries.
## When to Recover
[Section titled “When to Recover”](#when-to-recover)
```go
// HTTP handler - prevent one bad request from crashing server
func panicMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Error("HTTP handler panic", "path", r.URL.Path, "error", r)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unchecked\_error](/docs/reference/rules/go/unchecked-error/)
* [go.panic\_in\_library](/docs/reference/rules/go/panic-in-library/)
* [rust.panic\_in\_library](/docs/reference/rules/rust/panic-in-library/)
# go.channel_never_closed
> Detects channels that are created but never closed.
Stability Medium
Detects channels that are created but never closed, causing goroutines waiting on them to leak.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unclosed channels cause:
* **Goroutine leaks** — Receivers block forever waiting
* **Memory leaks** — Blocked goroutines hold references
* **Deadlocks** — Range loops never terminate
* **Resource exhaustion** — Slow accumulation of leaked goroutines
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (channel never closed)
func producer(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
}
// Channel never closed - consumers block forever!
}
func consumer(ch chan int) {
for v := range ch { // Blocks forever after producer done
process(v)
}
}
```
```go
// ✅ After (channel closed)
func producer(ch chan int) {
defer close(ch)
for i := 0; i < 10; i++ {
ch <- i
}
}
func consumer(ch chan int) {
for v := range ch { // Terminates when channel closed
process(v)
}
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `make(chan T)` without corresponding `close()`
* Channels passed to functions without closing
* Range loops over channels that are never closed
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds `defer close(ch)`:
```go
func produce(ch chan<- int) {
defer close(ch)
// ... produce values
}
```
Tip
Only close channels from the sender side, never from the receiver. Closing a closed channel panics.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
* [go.unbounded\_goroutines](/docs/reference/rules/go/unbounded-goroutines/)
# go.missing_circuit_breaker
> Detects HTTP client calls without circuit breaker protection.
Stability High
Detects HTTP client calls to external services without circuit breaker protection, which can cause cascading failures.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without circuit breakers:
* **Cascading failures** — One slow service brings down everything
* **Resource exhaustion** — Goroutines pile up waiting for responses
* **Extended outages** — Failing service never gets time to recover
* **Poor user experience** — All requests slow down, not just affected ones
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no circuit breaker)
func fetchData(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
return io.ReadAll(resp.Body)
}
```
```go
// ✅ After (with circuit breaker)
import "github.com/sony/gobreaker"
var cb = gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "api",
MaxRequests: 5,
Interval: 10 * time.Second,
Timeout: 30 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures > 5
},
})
func fetchData(url string) ([]byte, error) {
result, err := cb.Execute(func() (interface{}, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
return io.ReadAll(resp.Body)
})
if err != nil {
return nil, err
}
return result.([]byte), nil
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* HTTP calls without circuit breaker wrapper
* gRPC client calls without breakers
* External service integrations without failure isolation
* Database connections without circuit protection
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates patches using `github.com/sony/gobreaker`:
```go
import "github.com/sony/gobreaker"
var circuitBreaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "external-api",
MaxRequests: 3,
Timeout: 60 * time.Second,
})
```
Tip
Circuit breakers have three states: Closed (normal), Open (failing fast), Half-Open (testing recovery). Configure thresholds based on your service’s SLOs.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.http\_missing\_timeout](/docs/reference/rules/go/http-missing-timeout/)
* [go.unbounded\_retry](/docs/reference/rules/go/unbounded-retry/)
* [python.circuit\_breaker](/docs/reference/rules/python/circuit-breaker/)
# go.concurrent_map_access
> Detects concurrent map access without synchronization.
Correctness Critical
Detects concurrent access to maps without proper synchronization, which causes fatal runtime panics.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Concurrent map access causes:
* **Fatal panics** — Concurrent map read/write is a fatal error in Go
* **Service crashes** — No recovery possible from this panic
* **Intermittent failures** — Only manifests under load
* **Hard to reproduce** — Depends on goroutine timing
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (concurrent access)
var cache = make(map[string]interface{})
func handler(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("key")
cache[key] = "value" // Fatal error if concurrent!
}
```
```go
// ✅ After (sync.Map)
var cache sync.Map
func handler(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("key")
cache.Store(key, "value") // Thread-safe
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Map writes in goroutines
* Map access in HTTP handlers (implicitly concurrent)
* Missing mutex protection on maps
* Maps shared across goroutines
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates thread-safe alternatives:
```go
// Option 1: sync.Map (best for many goroutines, infrequent writes)
var cache sync.Map
// Option 2: RWMutex (best for frequent reads, rare writes)
type SafeMap struct {
mu sync.RWMutex
data map[string]interface{}
}
func (m *SafeMap) Get(key string) interface{} {
m.mu.RLock()
defer m.mu.RUnlock()
return m.data[key]
}
func (m *SafeMap) Set(key string, val interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
m.data[key] = val
}
```
Caution
Use `go run -race` or `go test -race` to detect data races during development. The race detector catches issues that static analysis may miss.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.race\_condition](/docs/reference/rules/go/race-condition/)
* [go.global\_mutable\_state](/docs/reference/rules/go/global-mutable-state/)
# go.context_background
> Detects inappropriate use of context.Background() where proper context should be passed.
Stability Medium
Detects inappropriate use of `context.Background()` where a proper context should be passed.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Context carries cancellation, deadlines, and request-scoped values:
* **Lost cancellation** — Parent cancellation doesn’t propagate
* **Leaked resources** — Operations continue after request ended
* **No deadline propagation** — Request deadline ignored
* **Missing tracing** — Request tracing lost
Using `context.Background()` breaks the context chain that enables Go’s graceful shutdown and timeout patterns.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
// ctx ignored, context.Background() used instead
return s.db.QueryContext(context.Background(), "SELECT...", id)
}
```
If the caller’s context is cancelled, the database query continues running.
```go
// ✅ After
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
return s.db.QueryContext(ctx, "SELECT...", id)
}
```
Now cancellation and deadlines propagate correctly.
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `context.Background()` when a context parameter is available
* `context.TODO()` in production code (meant for development)
* HTTP handlers not passing request context
* gRPC handlers ignoring the context parameter
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault replaces `context.Background()` with the available context parameter.
## When Background Is Acceptable
[Section titled “When Background Is Acceptable”](#when-background-is-acceptable)
```go
// Main function initialization
func main() {
ctx := context.Background()
server.Start(ctx)
}
// Long-running background jobs
func StartBackgroundWorker() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
runWorker(ctx)
}
// Tests
func TestSomething(t *testing.T) {
ctx := context.Background()
// ...
}
```
## Best Practices
[Section titled “Best Practices”](#best-practices)
```go
// HTTP handlers: use request context
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.service.GetUser(r.Context(), userID)
}
// gRPC handlers: context is first parameter
func (s *server) GetUser(ctx context.Context, req *pb.Request) (*pb.Response, error) {
return s.service.GetUser(ctx, req.Id)
}
// Chain context through all calls
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
user, err := s.cache.Get(ctx, id)
if err != nil {
user, err = s.db.Query(ctx, id)
}
return user, err
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
* [go.http\_missing\_timeout](/docs/reference/rules/go/http-missing-timeout/)
# go.cpu_in_hot_path
> Detects CPU-intensive operations in hot code paths.
Performance Medium
Detects CPU-intensive operations (reflection, regex, JSON marshaling) in frequently-called code paths.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
CPU work in hot paths causes:
* **Increased latency** — P99 latency spikes
* **Reduced throughput** — Less requests per second
* **Higher costs** — Need more instances to handle load
* **Poor scaling** — Performance degrades under load
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (expensive in loop)
func processItems(items []Item) {
for _, item := range items {
json.Marshal(item) // Reflection on every iteration
reflect.TypeOf(item) // Even more expensive
}
}
```
```go
// ✅ After (optimized)
func processItems(items []Item) {
// Batch marshal if needed
data, _ := json.Marshal(items)
// Or pre-compute type information
itemType := reflect.TypeOf(Item{})
for _, item := range items {
// Use cached type info
}
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `reflect` package usage in loops
* JSON marshal/unmarshal in hot paths
* Regex operations in loops (use pre-compiled patterns)
* Hash computations in tight loops
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault suggests moving expensive operations out of loops or pre-computing values.
Tip
Use code generation tools like `easyjson` or `ffjson` to avoid runtime reflection for JSON marshaling in performance-critical paths.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.regex\_compile](/docs/reference/rules/go/regex-compile/)
* [go.reflect\_in\_hot\_path](/docs/reference/rules/go/reflect-in-hot-path/)
* [python.cpu\_in\_event\_loop](/docs/reference/rules/python/cpu-in-event-loop/)
# go.defer_in_loop
> Detects defer statements inside loops.
Performance Medium
Detects `defer` statements inside loops.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Deferred calls accumulate until the function returns, not until the loop iteration ends:
* **Resource exhaustion** — File handles accumulate until function exits
* **Memory leak** — Deferred closures hold references
* **Crash under load** — Works fine with 10 items, crashes with 10,000
* **Counter-intuitive** — Common misconception even among experienced Go devs
This is one of Go’s most common gotchas.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
for _, file := range files {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close() // Doesn't close until function returns!
// Process file...
}
```
If you have 10,000 files, you open 10,000 file handles before closing any.
```go
// ✅ After (closure)
for _, file := range files {
func() {
f, err := os.Open(file)
if err != nil {
return
}
defer f.Close()
// Process file...
}()
}
```
```go
// ✅ After (explicit close)
for _, file := range files {
f, err := os.Open(file)
if err != nil {
return err
}
// Process file...
f.Close()
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `defer` inside `for` loops
* `defer` inside `range` loops
* Nested loop defer patterns
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault wraps the loop body in an immediately-invoked function literal (IIFE).
## Best Practices
[Section titled “Best Practices”](#best-practices)
```go
// Extract to helper function
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// Process...
return nil
}
for _, file := range files {
if err := processFile(file); err != nil {
return err
}
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
# go.echo.missing_middleware
> Detects Echo apps without essential middleware.
Stability Medium
Detects Echo apps without essential middleware.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Missing middleware:
* **No error handling** — Unhandled panics crash server
* **No request logging** — Can’t debug issues
* **No request timeout** — Slow requests block server
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no middleware)
e := echo.New()
e.GET("/", handler)
```
```go
// ✅ After (with essential middleware)
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Timeout: 30 * time.Second,
}))
e.GET("/", handler)
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Missing Recover middleware
* Missing Logger middleware
* Missing Timeout middleware
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.graceful\_shutdown](/docs/reference/rules/go/graceful-shutdown/)
# go.echo.request_validation
> Detects Echo handlers without request validation.
Correctness Medium
Detects Echo handlers without request validation.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Missing validation:
* **Invalid data accepted** — Bad input reaches business logic
* **Security vulnerabilities** — Unvalidated input exploitable
* **Runtime errors** — Type mismatches cause panics
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no validation)
func CreateUser(c echo.Context) error {
var req UserRequest
c.Bind(&req) // No validation!
return createUser(req)
}
```
```go
// ✅ After (with validation)
func CreateUser(c echo.Context) error {
var req UserRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if err := c.Validate(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return createUser(req)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Bind() without Validate()
* Missing validation on request body
* No error handling on bind
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.gin.missing\_validation](/docs/reference/rules/go/gin-missing-validation/)
# go.empty_critical_section
> Detects mutex locks with empty or trivial critical sections.
Performance Medium
Detects mutex locks protecting empty or trivial critical sections, causing unnecessary contention.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Empty critical sections cause:
* **Unnecessary contention** — Goroutines block for nothing
* **Deadlock risk** — Complex lock patterns with no benefit
* **Performance degradation** — Lock overhead without protection
* **Code smell** — Often indicates incomplete implementations
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (pointless locking)
var mu sync.Mutex
func process() {
mu.Lock()
defer mu.Unlock()
// Nothing protected!
}
// Also problematic
func update() {
mu.Lock()
mu.Unlock() // Immediate unlock
doWork() // Work happens outside lock
}
```
```go
// ✅ After (meaningful critical section)
var mu sync.Mutex
var counter int
func process() {
mu.Lock()
defer mu.Unlock()
counter++ // Actually protected
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `Lock()`/`Unlock()` with nothing between them
* `defer mu.Unlock()` with no protected operations
* Critical sections with only logging
* Lock/unlock without shared state access
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault flags these for manual review since the fix depends on intent:
```go
// Option 1: Remove unnecessary lock
func process() {
doWork() // If no shared state
}
// Option 2: Add protected operations
func process() {
mu.Lock()
sharedData = newValue
mu.Unlock()
doWork()
}
```
Tip
If you’re using locks for synchronization (not data protection), consider using `sync.WaitGroup`, channels, or `sync.Cond` instead.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.race\_condition](/docs/reference/rules/go/race-condition/)
* [go.concurrent\_map\_access](/docs/reference/rules/go/concurrent-map-access/)
# go.ephemeral_filesystem_write
> Detects filesystem writes that may be lost in containers.
Stability Medium
Detects filesystem writes to ephemeral locations in containerized environments.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Ephemeral writes:
* **Lost on restart** — Container restarts lose local files
* **Not shared** — Multiple instances don’t share files
* **Lost on scale** — New pods don’t have the data
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (ephemeral)
func saveFile(data []byte) error {
return os.WriteFile("/tmp/data.json", data, 0644)
}
```
```go
// ✅ After (persistent storage)
func saveFile(ctx context.Context, data []byte) error {
// Use S3 or mounted volume
_, err := s3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String("my-bucket"),
Key: aws.String("data.json"),
Body: bytes.NewReader(data),
})
return err
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `os.WriteFile()` to local paths
* `os.Create()` for persistent data
* `ioutil.WriteFile()` in containerized apps
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.ephemeral\_filesystem\_write](/docs/reference/rules/python/ephemeral-filesystem-write/)
# go.error_type_assertion
> Detects error type assertions without using errors.As().
Correctness Medium
Detects error type assertions using direct type assertion instead of `errors.As()`.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Direct type assertions on errors:
* **Miss wrapped errors** — Wrapped errors don’t match direct type checks
* **Break error chains** — Go 1.13+ error wrapping is ignored
* **Lose error context** — Miss errors wrapped with `fmt.Errorf("%w")`
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (misses wrapped errors)
if e, ok := err.(*MyError); ok {
handleMyError(e)
}
```
```go
// ✅ After (handles wrapped errors)
var myErr *MyError
if errors.As(err, &myErr) {
handleMyError(myErr)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `err.(*Type)` type assertions
* `switch err.(type)` without unwrapping
* Missing `errors.As()` for custom error types
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault replaces type assertions with `errors.As()`:
```go
var e *MyError
if errors.As(err, &e) {
// ...
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.sentinel\_error\_comparison](/docs/reference/rules/go/sentinel-error-comparison/)
* [go.unchecked\_error](/docs/reference/rules/go/unchecked-error/)
# go.gin.missing_validation
> Detects Gin handlers that bind request data without validation tags.
Correctness Medium
Detects Gin handlers that bind request data without validation tags.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unvalidated input causes problems:
* **Invalid data accepted** — Garbage in, garbage out
* **Null pointer panics** — Missing required fields cause crashes
* **Business logic errors** — Constraints violated downstream
* **Security vulnerabilities** — Unexpected input exploits assumptions
Gin has built-in validation through struct tags—use it.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
type CreateUserRequest struct {
Email string `json:"email"`
Password string `json:"password"`
Age int `json:"age"`
}
func CreateUser(c *gin.Context) {
var req CreateUserRequest
c.ShouldBindJSON(&req) // No validation
// Empty email? Negative age? Anything goes.
}
```
```go
// ✅ After
type CreateUserRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Age int `json:"age" binding:"gte=0,lte=150"`
}
func CreateUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Email is valid, password has 8+ chars, age is reasonable
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Struct bindings without `binding:` tags
* Missing `required` on essential fields
* ShouldBind without error checking
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds common validation tags based on field names and types.
## Common Validation Tags
[Section titled “Common Validation Tags”](#common-validation-tags)
```go
type Request struct {
// Required fields
Name string `binding:"required"`
// String constraints
Email string `binding:"required,email"`
URL string `binding:"url"`
UUID string `binding:"uuid"`
// Numeric constraints
Age int `binding:"gte=0,lte=150"`
Count int `binding:"min=1,max=100"`
// Length constraints
Password string `binding:"min=8,max=128"`
Code string `binding:"len=6"`
// Enums
Status string `binding:"oneof=pending active done"`
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unchecked\_error](/docs/reference/rules/go/unchecked-error/)
# go.gin.request_validation
> Detects Gin handlers without request validation.
Correctness High
Detects Gin handlers without request validation.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Missing request validation:
* **Invalid data** — Malformed input causes errors
* **Security risks** — Unvalidated input enables attacks
* **Poor UX** — Users get cryptic errors
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no validation)
func CreateUser(c *gin.Context) {
var user User
c.BindJSON(&user) // No error handling!
// process user...
}
```
```go
// ✅ After (with validation)
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=1,max=100"`
Email string `json:"email" binding:"required,email"`
}
func CreateUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
// process validated request...
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* BindJSON without error handling
* Missing struct validation tags
* Handlers without request binding
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add proper ShouldBind patterns with error handling.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.echo.request\_validation](/docs/reference/rules/go/echo-request-validation/)
# go.global_mutable_state
> Detects global mutable variables that can cause race conditions.
Correctness High
Detects global mutable state that can cause race conditions in concurrent code.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Global mutable state causes:
* **Race conditions** — Concurrent access without synchronization
* **Testing difficulties** — Tests affect each other
* **Hidden dependencies** — Functions have implicit state
* **Unpredictable behavior** — State changes unexpectedly
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (unsafe global state)
var cache = make(map[string]interface{})
func Get(key string) interface{} {
return cache[key] // Race condition!
}
func Set(key string, value interface{}) {
cache[key] = value // Race condition!
}
```
```go
// ✅ After (thread-safe)
var cache sync.Map
func Get(key string) (interface{}, bool) {
return cache.Load(key)
}
func Set(key string, value interface{}) {
cache.Store(key, value)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Package-level `var` with mutable types (maps, slices)
* Global variables accessed from multiple goroutines
* Missing mutex protection on shared state
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates thread-safe alternatives:
```go
// Option 1: sync.Map for concurrent maps
var cache sync.Map
// Option 2: Mutex-protected struct
type SafeCache struct {
mu sync.RWMutex
data map[string]interface{}
}
func (c *SafeCache) Get(key string) interface{} {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}
```
Tip
Prefer dependency injection over global state. Pass state explicitly to make dependencies clear and testing easier.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.race\_condition](/docs/reference/rules/go/race-condition/)
* [go.concurrent\_map\_access](/docs/reference/rules/go/concurrent-map-access/)
* [python.global\_mutable\_state](/docs/reference/rules/python/global-mutable-state/)
# go.gorm.connection_pool
> Detects missing or misconfigured GORM connection pool settings.
Scalability High
Detects missing or misconfigured GORM connection pool settings.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Missing connection pool configuration:
* **Connection exhaustion** — Too many connections overwhelm database
* **Resource waste** — Idle connections consume memory
* **Timeouts** — No connection reuse leads to slow queries
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no pool configuration)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
```
```go
// ✅ After (with connection pool settings)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
sqlDB, err := db.DB()
if err != nil {
log.Fatal(err)
}
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
sqlDB.SetConnMaxIdleTime(10 * time.Minute)
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Missing SetMaxOpenConns
* Missing SetMaxIdleConns
* Missing SetConnMaxLifetime
* Unreasonable pool size values
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add appropriate connection pool configuration based on common best practices.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.gorm.session\_management](/docs/reference/rules/go/gorm-session-management/)
* [go.gorm.query\_timeout](/docs/reference/rules/go/gorm-query-timeout/)
# go.gorm.n_plus_one
> Detects N+1 query patterns in GORM code.
Performance High
Detects N+1 query patterns in GORM code.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
N+1 queries:
* **Performance degradation** — Linear query growth per record
* **Database overload** — Excessive roundtrips saturate connections
* **Latency spikes** — Each query adds network overhead
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (N+1 queries)
var users []User
db.Find(&users)
for _, user := range users {
var orders []Order
db.Where("user_id = ?", user.ID).Find(&orders) // Query per user!
user.Orders = orders
}
```
```go
// ✅ After (eager loading with Preload)
var users []User
db.Preload("Orders").Find(&users)
```
```go
// ✅ Alternative (using Joins)
var users []User
db.Joins("Orders").Find(&users)
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Queries inside loops accessing related data
* Missing Preload for associations
* Repeated queries with only ID varying
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can suggest Preload patterns for detected N+1 queries.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.gorm.query\_timeout](/docs/reference/rules/go/gorm-query-timeout/)
* [go.transaction\_boundary](/docs/reference/rules/go/transaction-boundary/)
# go.gorm.query_timeout
> Detects GORM queries without timeout configuration.
Stability High
Detects GORM queries without timeout configuration.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Queries without timeouts:
* **Resource blocking** — Slow queries hold connections indefinitely
* **Cascading failures** — Database issues propagate to application
* **Poor user experience** — Requests hang without feedback
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no query timeout)
func GetUser(id uint) (*User, error) {
var user User
err := db.First(&user, id).Error
return &user, err
}
```
```go
// ✅ After (with context timeout)
func GetUser(ctx context.Context, id uint) (*User, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
var user User
err := db.WithContext(ctx).First(&user, id).Error
return &user, err
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* GORM queries without context
* Missing WithContext calls
* Hardcoded long timeout values
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can wrap queries with context timeout patterns.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.http\_timeout](/docs/reference/rules/go/http-timeout/)
* [go.gorm.connection\_pool](/docs/reference/rules/go/gorm-connection-pool/)
# go.gorm.session_management
> Detects improper GORM session management patterns.
Stability High
Detects improper GORM session management patterns.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Improper session management:
* **Connection leaks** — Sessions not closed properly
* **Stale data** — Reusing cached session state
* **Race conditions** — Sharing sessions across goroutines
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (reusing global db instance unsafely)
var db *gorm.DB
func GetUser(id uint) User {
var user User
db.First(&user, id) // May have stale session state
return user
}
```
```go
// ✅ After (using fresh session)
var db *gorm.DB
func GetUser(id uint) User {
var user User
db.Session(&gorm.Session{}).First(&user, id)
return user
}
// Or with context
func GetUserWithContext(ctx context.Context, id uint) User {
var user User
db.WithContext(ctx).First(&user, id)
return user
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Sharing DB instances across goroutines without session isolation
* Missing WithContext in request handlers
* Stale session reuse patterns
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.gorm.connection\_pool](/docs/reference/rules/go/gorm-connection-pool/)
* [go.transaction\_boundary](/docs/reference/rules/go/transaction-boundary/)
# go.goroutine_leak
> Detects goroutines that may never terminate.
Stability High Causes Production Outages
Detects goroutines that may never terminate (blocking on channels without cancellation).
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Leaked goroutines accumulate over time:
* **Memory growth** — Each goroutine uses \~2KB stack minimum
* **Resource exhaustion** — Eventually crashes the application
* **Hard to detect** — No obvious symptoms until it’s too late
* **Slow degradation** — Performance degrades gradually
A goroutine leak is like a memory leak, but worse because goroutines also hold references to other resources.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
func startWorker() {
go func() {
for {
msg := <-messages // Blocks forever if channel closes
process(msg)
}
}()
}
```
If `messages` is never closed and nothing sends, this goroutine lives forever.
```go
// ✅ After
func startWorker(ctx context.Context) {
go func() {
for {
select {
case msg := <-messages:
process(msg)
case <-ctx.Done():
return
}
}
}()
}
```
The goroutine exits when context is cancelled.
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Goroutines blocking on channel receive without `select`
* Goroutines without cancellation mechanism
* Channel sends without corresponding receives
* Forever loops without exit conditions
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add context-based cancellation to goroutine patterns when the blocking pattern is clearly identified.
Note
Unfault focuses on common patterns like channel receives without select and infinite loops. Complex concurrency patterns may require manual review.
## Common Leak Patterns
[Section titled “Common Leak Patterns”](#common-leak-patterns)
```go
// LEAK: Unbuffered channel with no receiver
ch := make(chan int)
go func() {
ch <- 1 // Blocks forever
}()
// LEAK: Range over channel never closed
go func() {
for v := range ch { // Blocks forever at end
process(v)
}
}()
// LEAK: Select without done case
go func() {
select {
case v := <-ch:
process(v)
} // Only processes once, then exits - but what if ch never sends?
}()
```
## Safe Patterns
[Section titled “Safe Patterns”](#safe-patterns)
```go
// Timeout
select {
case v := <-ch:
process(v)
case <-time.After(30 * time.Second):
return // Give up after timeout
}
// Context cancellation
select {
case v := <-ch:
process(v)
case <-ctx.Done():
return // Cancelled by parent
}
// Buffered channels for fire-and-forget
ch := make(chan int, 1)
ch <- 1 // Doesn't block
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unbounded\_goroutines](/docs/reference/rules/go/unbounded-goroutines/)
* [go.context\_background](/docs/reference/rules/go/context-background/)
# go.missing_graceful_shutdown
> Detects HTTP servers without graceful shutdown handling.
Stability High
Detects HTTP servers that don’t handle SIGTERM for graceful shutdown, causing dropped requests during deployments.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without graceful shutdown:
* **Dropped requests** — In-flight requests are terminated mid-processing
* **Data loss** — Partial writes, uncommitted transactions
* **Connection errors** — Clients receive connection reset errors
* **Deployment failures** — Rolling updates cause user-visible errors
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no graceful shutdown)
func main() {
http.ListenAndServe(":8080", handler)
}
```
```go
// ✅ After (graceful shutdown)
func main() {
server := &http.Server{
Addr: ":8080",
Handler: handler,
}
// Start server in goroutine
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exited gracefully")
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `http.ListenAndServe()` without signal handling
* Missing `server.Shutdown()` calls
* `log.Fatal()` in signal handlers (prevents cleanup)
* Hardcoded exits without cleanup
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates patches that add signal handling and graceful shutdown:
```go
import (
"context"
"os"
"os/signal"
"syscall"
)
// Graceful shutdown setup
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
```
Tip
Set Kubernetes `terminationGracePeriodSeconds` to match or exceed your server’s shutdown timeout to avoid SIGKILL during deployments.
## Kubernetes Configuration
[Section titled “Kubernetes Configuration”](#kubernetes-configuration)
```yaml
spec:
terminationGracePeriodSeconds: 30
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.http\_missing\_timeout](/docs/reference/rules/go/http-missing-timeout/)
* [python.graceful\_shutdown](/docs/reference/rules/python/graceful-shutdown/)
* [rust.tokio.missing\_graceful\_shutdown](/docs/reference/rules/rust/tokio-missing-graceful-shutdown/)
# go.grpc.missing_deadline
> Detects gRPC calls without deadline or timeout.
Stability Critical
Detects gRPC calls without deadline or timeout.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
gRPC calls without deadlines:
* **Resource exhaustion** — Hanging calls consume connections
* **Cascading failures** — Slow services block callers indefinitely
* **No failure feedback** — Clients wait forever
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no deadline)
resp, err := client.GetUser(context.Background(), req)
```
```go
// ✅ After (with deadline)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetUser(ctx, req)
if err != nil {
if status.Code(err) == codes.DeadlineExceeded {
// Handle timeout
}
return nil, err
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* gRPC client calls with context.Background()
* Missing context.WithTimeout or WithDeadline
* Unreasonably long timeout values
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can wrap gRPC calls with appropriate timeout contexts.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.http\_timeout](/docs/reference/rules/go/http-timeout/)
* [go.context\_background](/docs/reference/rules/go/context-background/)
# go.halstead_complexity
> Analyzes Halstead complexity metrics to identify hard-to-maintain functions.
Maintainability Low
Analyzes Halstead complexity metrics to identify hard-to-maintain functions.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
High complexity functions have measurable consequences:
* **More bugs** — Studies show defect density increases with complexity
* **Slower development** — Takes longer to understand and modify
* **Testing burden** — More paths require more test cases
* **Review difficulty** — Code reviewers miss issues in complex code
Halstead metrics provide objective measures that correlate with maintenance cost.
## Halstead Metrics
[Section titled “Halstead Metrics”](#halstead-metrics)
* **Difficulty (D)** — How hard to write or understand
* **Effort (E)** — Mental effort required
* **Volume (V)** — Information content
* **Vocabulary (n)** — Unique operators and operands
* **Length (N)** — Total operators and operands
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (high complexity)
func processOrder(order *Order, user *User, inv *Inventory, pay *Payment) error {
if order.Status != "pending" {
return errors.New("invalid status")
}
if !user.Verified {
return errors.New("user not verified")
}
if user.Balance < order.Total {
return errors.New("insufficient balance")
}
for _, item := range order.Items {
stock, ok := inv.Stock[item.ID]
if !ok || stock < item.Qty {
return fmt.Errorf("item %s out of stock", item.ID)
}
inv.Stock[item.ID] -= item.Qty
if inv.Stock[item.ID] < 10 {
notifyRestock(item.ID)
}
}
if err := pay.Charge(user.ID, order.Total); err != nil {
for _, item := range order.Items {
inv.Stock[item.ID] += item.Qty // Rollback
}
return err
}
order.Status = "complete"
return nil
}
```
```go
// ✅ After (decomposed)
func processOrder(ctx *OrderContext) error {
if err := validateOrder(ctx.Order, ctx.User); err != nil {
return err
}
reserved, err := reserveInventory(ctx.Order, ctx.Inventory)
if err != nil {
return err
}
if err := processPayment(ctx.User, ctx.Order.Total, ctx.Payment); err != nil {
releaseInventory(reserved, ctx.Inventory)
return err
}
ctx.Order.Status = "complete"
return nil
}
func validateOrder(order *Order, user *User) error {
if order.Status != "pending" {
return errors.New("invalid status")
}
if !user.Verified {
return errors.New("user not verified")
}
if user.Balance < order.Total {
return errors.New("insufficient balance")
}
return nil
}
```
Each function has a single responsibility and lower complexity.
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Functions exceeding difficulty threshold
* High effort scores
* Deeply nested control flow
* Long parameter lists
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault flags high-complexity functions. Refactoring is manual but guided.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.halstead\_complexity](/docs/reference/rules/python/halstead-complexity/)
* [rust.halstead\_complexity](/docs/reference/rules/rust/halstead-complexity/)
* [typescript.halstead\_complexity](/docs/reference/rules/typescript/halstead-complexity/)
# go.hardcoded_secrets
> Detects hardcoded API keys, passwords, and secrets in source code.
Security Critical Common in Incidents
Detects hardcoded API keys, passwords, and other secrets in source code.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Hardcoded secrets are a severe security risk:
* **Version control exposure** — Secrets committed to git are visible to everyone with access
* **Cannot rotate** — Changing a secret requires code changes and deployment
* **Audit impossible** — No way to track secret access
* **Breach amplification** — One compromised repo exposes all services
Secrets in code get leaked through backups, logs, error messages, and repository access.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
const (
APIKey = "sk_live_abc123xyz"
Password = "supersecret"
DatabaseURL = "postgres://user:pass@host/db"
)
client := stripe.NewClient("sk_live_abc123xyz")
```
```go
// ✅ After
import "os"
var (
APIKey = os.Getenv("STRIPE_API_KEY")
Password = os.Getenv("DB_PASSWORD")
DatabaseURL = os.Getenv("DATABASE_URL")
)
client := stripe.NewClient(os.Getenv("STRIPE_API_KEY"))
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Variables named `password`, `secret`, `key`, `token`, etc.
* Strings matching API key patterns (AWS, Stripe, GitHub, etc.)
* Database connection strings with credentials
* JWT secrets and signing keys
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can replace hardcoded values with `os.Getenv()` calls when the pattern is recognized.
Note
Unfault uses pattern matching for known API key formats and sensitive variable names. Test data and clearly marked example values may still be flagged for review.
## Best Practices
[Section titled “Best Practices”](#best-practices)
```go
// Use environment variables
apiKey := os.Getenv("API_KEY")
if apiKey == "" {
log.Fatal("API_KEY environment variable required")
}
// Or use a secrets manager
import "github.com/aws/aws-sdk-go/service/secretsmanager"
secret, err := sm.GetSecretValue(&secretsmanager.GetSecretValueInput{
SecretId: aws.String("my-secret"),
})
// Configuration libraries
import "github.com/spf13/viper"
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
apiKey := viper.GetString("API_KEY")
```
## Common Secret Patterns
[Section titled “Common Secret Patterns”](#common-secret-patterns)
| Pattern | Example |
| -------------- | ---------------------------- |
| AWS Access Key | `AKIA...` |
| AWS Secret Key | 40-char base64 |
| Stripe Key | `sk_live_...`, `pk_live_...` |
| GitHub Token | `ghp_...`, `github_pat_...` |
| JWT Secret | Long random strings |
| Database URL | `postgres://user:pass@...` |
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [rust.hardcoded\_secrets](/docs/reference/rules/rust/hardcoded-secrets/)
* [typescript.hardcoded\_secrets](/docs/reference/rules/typescript/hardcoded-secrets/)
# go.http_missing_timeout
> Detects HTTP client usage without timeout configuration.
Stability High Common in Incidents
Detects HTTP client usage without explicit timeout configuration.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
HTTP requests without timeouts can hang indefinitely:
* **Connection pool exhaustion** — Stuck requests hold connections
* **Goroutine leaks** — Waiting goroutines accumulate
* **Cascade failures** — One slow upstream brings down your service
* **Unresponsive service** — All workers blocked waiting
The default `http.Client` has no timeout. This is a dangerous default.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
client := &http.Client{}
resp, err := client.Get(url)
// Also bad: using http.Get directly
resp, err := http.Get(url)
```
If the server never responds, these calls wait forever.
```go
// ✅ After
client := &http.Client{
Timeout: 30 * time.Second,
}
resp, err := client.Get(url)
```
After 30 seconds, the request fails with a timeout error.
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `&http.Client{}` without Timeout field
* `http.Get()`, `http.Post()`, etc. (use default client)
* Client with Transport but no timeout
* Missing context deadline on requests
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add `Timeout: 30 * time.Second` to HTTP client initialization when configuring a default client.
Note
Unfault detects `&http.Client{}` without Timeout and direct usage of `http.Get()`. Clients using custom Transport configurations are flagged but may need manual timeout tuning.
## Best Practices
[Section titled “Best Practices”](#best-practices)
```go
// Overall request timeout
client := &http.Client{
Timeout: 30 * time.Second,
}
// Fine-grained control with Transport
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second, // Connection timeout
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
IdleConnTimeout: 90 * time.Second,
},
}
// Context-based timeout (per-request)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := client.Do(req)
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.http.missing\_timeout](/docs/reference/rules/python/http-missing-timeout/)
* [typescript.http\_missing\_timeout](/docs/reference/rules/typescript/http-missing-timeout/)
* [go.grpc.missing\_deadline](/docs/reference/rules/go/grpc-missing-deadline/)
# go.http_retry
> Detects HTTP calls without retry logic for transient failures.
Stability Medium
Detects HTTP client calls without retry logic, which fail on transient network issues.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without retries:
* **Transient failures become permanent** — Network blips cause request failures
* **Poor user experience** — Users see errors for recoverable issues
* **Reduced reliability** — 99.9% uptime requires handling temporary failures
* **Cascading issues** — One failed request may abort entire workflows
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no retry)
func fetchUser(id string) (*User, error) {
resp, err := http.Get(fmt.Sprintf("/users/%s", id))
if err != nil {
return nil, err // Fails on first transient error
}
// ...
}
```
```go
// ✅ After (with retry)
import "github.com/hashicorp/go-retryablehttp"
var client = retryablehttp.NewClient()
func fetchUser(id string) (*User, error) {
resp, err := client.Get(fmt.Sprintf("/users/%s", id))
if err != nil {
return nil, err
}
// ...
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* HTTP GET/POST without retry wrapper
* Missing retry on 5xx responses
* Missing retry on connection errors
* Direct `http.Client` usage without retry policy
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates patches using `go-retryablehttp`:
```go
import "github.com/hashicorp/go-retryablehttp"
client := retryablehttp.NewClient()
client.RetryMax = 3
client.RetryWaitMin = 1 * time.Second
client.RetryWaitMax = 30 * time.Second
```
Caution
Only retry idempotent operations (GET, PUT, DELETE). Retrying POST without idempotency keys can cause duplicate operations.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unbounded\_retry](/docs/reference/rules/go/unbounded-retry/)
* [go.circuit\_breaker](/docs/reference/rules/go/circuit-breaker/)
* [go.http\_missing\_timeout](/docs/reference/rules/go/http-missing-timeout/)
# go.idempotency_key
> Detects POST/PUT endpoints lacking idempotency key handling.
Correctness Medium
Detects state-modifying HTTP endpoints without idempotency key handling.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without idempotency keys:
* **Duplicate operations** — Retries create duplicate records
* **Double charges** — Payment retries charge multiple times
* **Data inconsistency** — Same request processed multiple times
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no idempotency)
func createOrder(w http.ResponseWriter, r *http.Request) {
order := parseOrder(r)
db.Create(&order) // Duplicate on retry!
}
```
```go
// ✅ After (with idempotency key)
func createOrder(w http.ResponseWriter, r *http.Request) {
idempotencyKey := r.Header.Get("Idempotency-Key")
if idempotencyKey == "" {
http.Error(w, "Idempotency-Key required", 400)
return
}
// Check if already processed
if result, ok := cache.Get(idempotencyKey); ok {
json.NewEncoder(w).Encode(result)
return
}
order := parseOrder(r)
db.Create(&order)
cache.Set(idempotencyKey, order, 24*time.Hour)
json.NewEncoder(w).Encode(order)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* POST/PUT handlers without idempotency key checks
* Payment processing without idempotency
* Order creation without duplicate protection
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates idempotency middleware:
```go
func IdempotencyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" || r.Method == "PUT" {
key := r.Header.Get("Idempotency-Key")
// Check/store key...
}
next.ServeHTTP(w, r)
})
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.idempotency\_key](/docs/reference/rules/python/idempotency-key/)
# go.large_response_memory
> Detects unbounded response body reads that can exhaust memory.
Stability Medium
Detects HTTP response body reads without size limits, which can exhaust memory.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unbounded response reads cause:
* **OOM crashes** — Malicious or broken servers return huge responses
* **DoS vulnerability** — Attackers control response size
* **Resource exhaustion** — Memory spikes affect other requests
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (unbounded)
resp, _ := http.Get(url)
body, _ := io.ReadAll(resp.Body) // Could be gigabytes!
```
```go
// ✅ After (bounded)
resp, _ := http.Get(url)
body, _ := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) // 10MB max
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `io.ReadAll(resp.Body)` without limits
* `ioutil.ReadAll()` without Content-Length check
* JSON decoding without size limits
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds `io.LimitReader`:
```go
const maxResponseSize = 10 * 1024 * 1024 // 10MB
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unbounded\_memory](/docs/reference/rules/go/unbounded-memory/)
* [python.large\_response\_memory](/docs/reference/rules/python/large-response-memory/)
# go.map_without_size_hint
> Detects map initialization without size hint when size is known.
Performance Low
Detects map initialization without capacity hint when the expected size is known.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Maps without size hints:
* **Repeated rehashing** — Map grows and rehashes as items added
* **Memory copying** — Each grow copies all entries
* **Fragmented memory** — Multiple allocations instead of one
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no hint)
result := make(map[string]int)
for _, item := range items {
result[item.Key] = item.Value
}
```
```go
// ✅ After (with hint)
result := make(map[string]int, len(items))
for _, item := range items {
result[item.Key] = item.Value
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `make(map[K]V)` in loops where size is known
* Map initialization followed by loop filling it
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds size hints:
```go
result := make(map[string]int, len(items))
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.slice\_append\_in\_loop](/docs/reference/rules/go/slice-append-in-loop/)
# go.missing_correlation_id
> Detects HTTP handlers without correlation ID propagation.
Observability Low
Detects HTTP handlers that don’t propagate correlation IDs for distributed tracing.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without correlation IDs:
* **Can’t trace requests** — Logs across services can’t be correlated
* **Debugging nightmare** — Finding related events is manual work
* **Slow incident response** — More time spent correlating logs
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no correlation ID)
func handler(w http.ResponseWriter, r *http.Request) {
log.Println("Processing request")
}
```
```go
// ✅ After (with correlation ID)
func handler(w http.ResponseWriter, r *http.Request) {
correlationID := r.Header.Get("X-Correlation-ID")
if correlationID == "" {
correlationID = uuid.New().String()
}
ctx := context.WithValue(r.Context(), "correlation_id", correlationID)
log.Printf("Processing request correlation_id=%s", correlationID)
w.Header().Set("X-Correlation-ID", correlationID)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* HTTP handlers without correlation ID extraction
* Log statements without correlation IDs
* Outgoing requests without ID forwarding
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.missing\_structured\_logging](/docs/reference/rules/go/missing-structured-logging/)
* [go.missing\_tracing](/docs/reference/rules/go/missing-tracing/)
* [python.missing\_correlation\_id](/docs/reference/rules/python/missing-correlation-id/)
# go.missing_structured_logging
> Detects usage of fmt.Println or log.Print instead of structured logging.
Observability Low
Detects usage of `fmt.Println`/`log.Print` instead of structured logging (zerolog/zap/slog).
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unstructured logs are hard to work with:
* **Not queryable** — Can’t search for specific fields
* **Not aggregatable** — Can’t count or group events
* **Not alertable** — Can’t set conditions on values
* **Parsing hell** — Regex extraction is fragile
Modern observability requires structured data with consistent fields.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
fmt.Printf("User %s logged in\n", userID)
log.Printf("Error: %v", err)
```
These are impossible to filter or aggregate in your logging system.
```go
// ✅ After (zerolog)
import "github.com/rs/zerolog/log"
log.Info().Str("user_id", userID).Msg("user logged in")
log.Error().Err(err).Msg("operation failed")
```
Now you can query `user_id:123` or count login events by user.
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `fmt.Print`, `fmt.Printf`, `fmt.Println`
* `log.Print`, `log.Printf`, `log.Println`
* `log.Fatal`, `log.Panic` (also replace with structured)
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault converts print statements to zerolog calls.
## Recommended Libraries
[Section titled “Recommended Libraries”](#recommended-libraries)
```go
// zerolog - Fast, structured, zero allocation
import "github.com/rs/zerolog/log"
log.Info().Str("key", "value").Msg("message")
// zap - Uber's high-performance logger
import "go.uber.org/zap"
logger, _ := zap.NewProduction()
logger.Info("message", zap.String("key", "value"))
// slog - Standard library (Go 1.21+)
import "log/slog"
slog.Info("message", "key", "value")
```
## Configuration
[Section titled “Configuration”](#configuration)
```go
// zerolog with pretty console for dev
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
// Production JSON output
log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.missing\_structured\_logging](/docs/reference/rules/python/missing-structured-logging/)
* [typescript.console\_in\_production](/docs/reference/rules/typescript/console-in-production/)
# go.missing_tracing
> Detects code without distributed tracing instrumentation.
Observability Low
Detects code without distributed tracing instrumentation for observability.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without tracing:
* **No visibility** — Can’t see request flow across services
* **Slow debugging** — Can’t identify bottlenecks
* **Missing metrics** — No latency data per operation
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no tracing)
func handleRequest(ctx context.Context) error {
data := fetchData(ctx)
return processData(data)
}
```
```go
// ✅ After (with OpenTelemetry)
import "go.opentelemetry.io/otel"
var tracer = otel.Tracer("my-service")
func handleRequest(ctx context.Context) error {
ctx, span := tracer.Start(ctx, "handleRequest")
defer span.End()
data := fetchData(ctx)
return processData(ctx, data)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* HTTP handlers without trace spans
* Database operations without spans
* Service calls without trace propagation
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.missing\_structured\_logging](/docs/reference/rules/go/missing-structured-logging/)
* [go.missing\_correlation\_id](/docs/reference/rules/go/missing-correlation-id/)
* [python.missing\_tracing](/docs/reference/rules/python/missing-tracing/)
# go.nethttp.missing_timeout
> Detects net/http clients and servers without timeout configuration.
Stability Critical
Detects net/http clients and servers without timeout configuration.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Missing HTTP timeouts:
* **Resource exhaustion** — Hanging connections consume resources
* **Slowloris attacks** — Servers vulnerable to slow clients
* **Cascading failures** — Slow dependencies block callers
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no timeout - client)
client := &http.Client{}
resp, err := client.Get("https://api.example.com/data")
// ❌ Before (no timeout - server)
server := &http.Server{
Addr: ":8080",
}
```
```go
// ✅ After (with timeout - client)
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
}).DialContext,
ResponseHeaderTimeout: 10 * time.Second,
IdleConnTimeout: 90 * time.Second,
},
}
// ✅ After (with timeout - server)
server := &http.Server{
Addr: ":8080",
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* http.Client without Timeout
* http.Server without ReadTimeout/WriteTimeout
* http.Transport without timeouts
* Use of http.DefaultClient
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add appropriate timeout configuration to HTTP clients and servers.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.http\_timeout](/docs/reference/rules/go/http-timeout/)
* [go.graceful\_shutdown](/docs/reference/rules/go/graceful-shutdown/)
# go.panic_in_library
> Detects panic() calls in library code that should return errors.
Stability High
Detects `panic()` calls in library code that should return errors instead.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Panics in libraries:
* **Crash calling applications** — Library panic terminates the app
* **Remove caller control** — Callers can’t handle errors gracefully
* **Violate Go conventions** — Libraries should return errors
* **Cause production outages** — One bad input crashes everything
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (panic in library)
func Parse(data []byte) Config {
if len(data) == 0 {
panic("empty data") // Crashes caller!
}
// ...
}
```
```go
// ✅ After (returns error)
func Parse(data []byte) (Config, error) {
if len(data) == 0 {
return Config{}, errors.New("empty data")
}
// ...
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `panic()` in exported functions
* `panic()` in package without `main`
* `panic()` for recoverable errors
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault converts panic to error return:
```go
func Process(input string) (Result, error) {
if input == "" {
return Result{}, errors.New("empty input")
}
// ...
}
```
Tip
Panic is acceptable for truly unrecoverable situations (programmer errors, invariant violations) but not for user input or external data.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.bare\_recover](/docs/reference/rules/go/bare-recover/)
* [rust.panic\_in\_library](/docs/reference/rules/rust/panic-in-library/)
# go.race_condition
> Detects potential race conditions from concurrent access to shared state.
Correctness High Common in Incidents
Detects potential race conditions from concurrent access to shared state without synchronization.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Race conditions cause some of the hardest bugs to find:
* **Intermittent failures** — May only happen under specific timing
* **Data corruption** — Concurrent writes produce garbage
* **Security vulnerabilities** — TOCTOU attacks exploit race windows
* **Impossible to reproduce** — Works in testing, fails in production
Go’s race detector catches some at runtime, but static analysis catches them earlier.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
var counter int
func incrementUnsafe() {
go func() {
counter++ // Race: concurrent read-modify-write
}()
}
```
Two goroutines incrementing simultaneously might both read the same value and write the same result.
```go
// ✅ After (mutex)
var (
counter int
mu sync.Mutex
)
func incrementSafe() {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
// ✅ After (atomic)
var counter int64
func incrementAtomic() {
go func() {
atomic.AddInt64(&counter, 1)
}()
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Shared variables accessed in goroutines without mutex
* Map access from multiple goroutines
* Struct field access without synchronization
* Closure capturing variables modified concurrently
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add mutex protection around shared variable access when the pattern allows safe transformation.
Note
Static race detection has inherent limitations. Unfault catches common patterns but recommends running `go test -race` for comprehensive coverage. Some advanced synchronization patterns may be flagged conservatively.
## Common Patterns
[Section titled “Common Patterns”](#common-patterns)
```go
// Atomic for simple counters
var count int64
atomic.AddInt64(&count, 1)
val := atomic.LoadInt64(&count)
// Mutex for complex state
type SafeCache struct {
mu sync.RWMutex
data map[string]string
}
func (c *SafeCache) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}
func (c *SafeCache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
// sync.Map for simple concurrent maps
var cache sync.Map
cache.Store("key", "value")
val, ok := cache.Load("key")
```
## Testing for Races
[Section titled “Testing for Races”](#testing-for-races)
```bash
# Run tests with race detector
go test -race ./...
# Build with race detector
go build -race
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.race\_condition](/docs/reference/rules/python/race-condition/)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
# go.rate_limiting
> Detects API endpoints without rate limiting protection.
Scalability Medium
Detects HTTP endpoints without rate limiting, which can be abused or overwhelmed.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without rate limiting:
* **DoS vulnerability** — Anyone can overwhelm your service
* **Resource exhaustion** — Uncontrolled traffic consumes all capacity
* **Unfair access** — One client can starve others
* **Cost explosion** — Cloud costs spike with traffic bursts
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no rate limiting)
func handler(w http.ResponseWriter, r *http.Request) {
// Any client can call this unlimited times
processRequest(w, r)
}
```
```go
// ✅ After (with rate limiting)
import "golang.org/x/time/rate"
var limiter = rate.NewLimiter(100, 10) // 100 req/sec, burst 10
func handler(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
processRequest(w, r)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* HTTP handlers without rate limit middleware
* Missing per-client rate limiting
* Expensive operations without throttling
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates rate limiting middleware:
```go
import "golang.org/x/time/rate"
// Per-client rate limiting
type ClientLimiter struct {
limiters map[string]*rate.Limiter
mu sync.RWMutex
rate rate.Limit
burst int
}
func (cl *ClientLimiter) GetLimiter(clientID string) *rate.Limiter {
cl.mu.RLock()
limiter, exists := cl.limiters[clientID]
cl.mu.RUnlock()
if exists {
return limiter
}
cl.mu.Lock()
limiter = rate.NewLimiter(cl.rate, cl.burst)
cl.limiters[clientID] = limiter
cl.mu.Unlock()
return limiter
}
```
Tip
Use Redis-based rate limiting for distributed systems where multiple instances share rate limit state.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unbounded\_goroutines](/docs/reference/rules/go/unbounded-goroutines/)
* [typescript.rate\_limiting](/docs/reference/rules/typescript/rate-limiting/)
# go.redis.connection_pool
> Detects missing or misconfigured Redis connection pool settings.
Scalability High
Detects missing or misconfigured Redis connection pool settings.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Missing connection pool configuration:
* **Connection exhaustion** — Too many connections overwhelm Redis
* **Resource waste** — Idle connections consume memory
* **Timeouts** — No pool limits lead to starvation
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no pool configuration)
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
```
```go
// ✅ After (with connection pool settings)
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
PoolSize: 100,
MinIdleConns: 10,
PoolTimeout: 30 * time.Second,
MaxRetries: 3,
})
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Missing PoolSize configuration
* Missing MinIdleConns
* Missing PoolTimeout
* Unreasonable pool size values
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add appropriate connection pool configuration based on common best practices.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.redis.missing\_ttl](/docs/reference/rules/go/redis-missing-ttl/)
* [go.unbounded\_cache](/docs/reference/rules/go/unbounded-cache/)
# go.redis.missing_ttl
> Detects Redis SET operations without TTL.
Scalability High
Detects Redis SET operations without TTL.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Redis keys without TTL:
* **Memory growth** — Keys accumulate indefinitely
* **OOM risk** — Redis runs out of memory
* **Stale data** — Old values never expire
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no TTL)
err := client.Set(ctx, "user:123", userData, 0).Err()
```
```go
// ✅ After (with TTL)
err := client.Set(ctx, "user:123", userData, 24*time.Hour).Err()
```
```go
// ✅ Alternative (SetEX for explicit expiration)
err := client.SetEx(ctx, "session:abc", sessionData, 30*time.Minute).Err()
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Set with duration 0 (no expiry)
* Missing expiration on cache keys
* SetNX without subsequent Expire
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add TTL parameters to Redis SET operations.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.redis.connection\_pool](/docs/reference/rules/go/redis-connection-pool/)
* [go.unbounded\_cache](/docs/reference/rules/go/unbounded-cache/)
# go.reflect_in_hot_path
> Detects reflection usage in performance-critical code paths.
Performance Medium
Detects `reflect` package usage in frequently-called code paths.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Reflection in hot paths:
* **10-100x slower** — Reflection bypasses compile-time optimizations
* **More allocations** — Reflect creates temporary objects
* **No inlining** — Defeats compiler optimizations
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (reflection in loop)
func processItems(items []interface{}) {
for _, item := range items {
v := reflect.ValueOf(item)
// Reflection on every iteration
}
}
```
```go
// ✅ After (type assertion)
func processItems(items []Item) {
for _, item := range items {
// Direct field access, no reflection
process(item.Name)
}
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `reflect.ValueOf()` in loops
* `reflect.TypeOf()` in handlers
* Reflection in hot code paths
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault suggests type assertions or interfaces:
```go
// Use type switch instead of reflection
switch v := item.(type) {
case string:
processString(v)
case int:
processInt(v)
}
```
Tip
If you need reflection, cache type information outside loops. Use code generation for serialization (e.g., `easyjson`).
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.cpu\_in\_hot\_path](/docs/reference/rules/go/cpu-in-hot-path/)
* [go.regex\_compile](/docs/reference/rules/go/regex-compile/)
# go.regex_compile
> Detects regex patterns compiled inside loops or hot paths.
Performance Medium
Detects regex patterns compiled repeatedly inside loops or frequently-called functions.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Compiling regex in hot paths:
* **Wastes CPU** — Regex compilation is expensive
* **Increases latency** — Each request pays compilation cost
* **Scales poorly** — Impact grows with traffic
* **Triggers GC pressure** — Allocates memory repeatedly
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (compiled every call)
func validateEmail(email string) bool {
pattern := regexp.MustCompile(`^[\w.-]+@[\w.-]+\.\w+$`)
return pattern.MatchString(email)
}
```
```go
// ✅ After (compiled once)
var emailPattern = regexp.MustCompile(`^[\w.-]+@[\w.-]+\.\w+$`)
func validateEmail(email string) bool {
return emailPattern.MatchString(email)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `regexp.Compile()` inside functions
* `regexp.MustCompile()` inside loops
* Repeated pattern compilation in handlers
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault moves regex compilation to package level:
```go
// Moved to package level
var _pattern = regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
```
Tip
Use `regexp.MustCompile()` at package level - it panics on invalid patterns, catching errors at startup rather than runtime.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.regex\_compile](/docs/reference/rules/python/regex-compile/)
* [rust.regex\_compile](/docs/reference/rules/rust/regex-compile/)
# go.sentinel_error_comparison
> Detects direct error string comparison instead of errors.Is().
Correctness Medium
Detects error comparisons using `==` or string matching instead of `errors.Is()`.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Direct error comparison:
* **Misses wrapped errors** — `fmt.Errorf("%w", err)` won’t match
* **Breaks error chains** — Go 1.13+ error wrapping is ignored
* **Fragile** — String comparisons break on message changes
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (broken with wrapped errors)
if err == sql.ErrNoRows {
return nil
}
if err.Error() == "not found" {
return nil
}
```
```go
// ✅ After (handles wrapped errors)
if errors.Is(err, sql.ErrNoRows) {
return nil
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `err == sentinel` comparisons
* `err.Error() == "..."` string comparisons
* Missing `errors.Is()` usage
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault replaces direct comparison with `errors.Is()`:
```go
if errors.Is(err, sql.ErrNoRows) {
// ...
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unchecked\_error](/docs/reference/rules/go/unchecked-error/)
* [go.error\_type\_assertion](/docs/reference/rules/go/error-type-assertion/)
# go.slice_append_in_loop
> Detects inefficient slice append patterns in loops.
Performance Low
Detects slice appends in loops without pre-allocation, causing repeated reallocations.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Appending without pre-allocation:
* **Multiple reallocations** — Slice grows exponentially, copying each time
* **Memory churn** — Old backing arrays become garbage
* **GC pressure** — More work for garbage collector
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (grows slice repeatedly)
var result []Item
for _, id := range ids {
result = append(result, fetchItem(id))
}
```
```go
// ✅ After (pre-allocated)
result := make([]Item, 0, len(ids))
for _, id := range ids {
result = append(result, fetchItem(id))
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `append()` in loops without `make([]T, 0, n)` initialization
* Growing slices without capacity hint when size is known
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds slice pre-allocation:
```go
result := make([]Item, 0, len(ids))
```
Tip
If the exact size is unknown, estimate a reasonable upper bound. Over-allocating slightly is better than multiple reallocations.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.slice\_memory\_leak](/docs/reference/rules/go/slice-memory-leak/)
* [go.map\_without\_size\_hint](/docs/reference/rules/go/map-without-size-hint/)
# go.slice_memory_leak
> Detects slice operations that can cause memory leaks.
Performance Medium
Detects slice operations that retain references to underlying arrays, causing memory leaks.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Slice memory leaks cause:
* **Hidden memory retention** — Small slice keeps large array alive
* **Gradual memory growth** — Leaks accumulate over time
* **Hard to diagnose** — Memory profiler shows slices, not retained arrays
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (retains entire array)
func getPrefix(data []byte) []byte {
return data[:10] // Keeps entire backing array!
}
```
```go
// ✅ After (copies to new slice)
func getPrefix(data []byte) []byte {
prefix := make([]byte, 10)
copy(prefix, data[:10])
return prefix // Only 10 bytes retained
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Returning slice of large input
* Storing sub-slices in long-lived structures
* Appending without copying when source is discarded
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates copy patterns:
```go
result := make([]T, len(subslice))
copy(result, subslice)
```
Tip
Use `slices.Clone()` (Go 1.21+) for cleaner copy syntax: `result := slices.Clone(data[:10])`
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.slice\_append\_in\_loop](/docs/reference/rules/go/slice-append-in-loop/)
* [go.unbounded\_memory](/docs/reference/rules/go/unbounded-memory/)
# go.sql_injection
> Detects SQL queries built with string concatenation.
Correctness Critical Common in Incidents
Detects SQL queries built with string concatenation instead of parameterized queries.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
SQL injection is a critical security vulnerability:
* **Data theft** — Attackers can read your entire database
* **Data destruction** — DROP TABLE, DELETE, UPDATE at will
* **Authentication bypass** — Log in as any user
* **Privilege escalation** — Gain admin access
One unparameterized query can compromise your entire system.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
query := "SELECT * FROM users WHERE id = " + userID
db.Query(query)
// Also bad
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", name)
```
If `userID` is `"1 OR 1=1"`, all users are returned. If `name` is `"'; DROP TABLE users; --"`, your data is gone.
```go
// ✅ After
db.Query("SELECT * FROM users WHERE id = ?", userID)
// Or with named parameters (sqlx)
db.NamedQuery("SELECT * FROM users WHERE name = :name", map[string]interface{}{"name": name})
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* String concatenation (`+`) in SQL query strings
* `fmt.Sprintf` with SQL keywords
* `strings.Replace` on query templates
* Variable interpolation in queries
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can convert concatenated queries to parameterized form when the query structure is unambiguous.
Note
Unfault avoids flagging known-safe patterns like constant queries and query builder methods. Dynamic table names may be flagged but require manual review.
## Database Driver Placeholders
[Section titled “Database Driver Placeholders”](#database-driver-placeholders)
```go
// PostgreSQL (lib/pq, pgx)
db.Query("SELECT * FROM users WHERE id = $1", id)
// MySQL
db.Query("SELECT * FROM users WHERE id = ?", id)
// SQLite
db.Query("SELECT * FROM users WHERE id = ?", id)
// Multiple parameters - PostgreSQL
db.Query("SELECT * FROM users WHERE id = $1 AND status = $2", id, status)
// Multiple parameters - MySQL
db.Query("SELECT * FROM users WHERE id = ? AND status = ?", id, status)
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.sql\_injection](/docs/reference/rules/python/sql-injection/)
* [rust.sql\_injection](/docs/reference/rules/rust/sql-injection/)
* [typescript.sql\_injection](/docs/reference/rules/typescript/sql-injection/)
# go.sync_dns_lookup
> Detects synchronous DNS lookups that can block goroutines.
Performance Medium
Detects synchronous DNS lookups that can block for seconds during resolution failures.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Synchronous DNS:
* **Blocks goroutines** — DNS can take seconds to timeout
* **Exhausts workers** — Blocked goroutines can’t serve requests
* **Cascades failures** — DNS issues affect all requests
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (synchronous)
func connect(host string) error {
addrs, err := net.LookupHost(host) // Can block for seconds
if err != nil {
return err
}
// ...
}
```
```go
// ✅ After (with timeout)
func connect(ctx context.Context, host string) error {
resolver := &net.Resolver{}
addrs, err := resolver.LookupHost(ctx, host) // Respects context timeout
if err != nil {
return err
}
// ...
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `net.LookupHost()` without context
* `net.LookupAddr()` without timeout
* DNS lookups in critical paths
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds context-aware DNS resolution:
```go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resolver := &net.Resolver{}
addrs, err := resolver.LookupHost(ctx, host)
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.http\_missing\_timeout](/docs/reference/rules/go/http-missing-timeout/)
* [python.sync\_dns\_lookup](/docs/reference/rules/python/sync-dns-lookup/)
# go.transaction_boundary
> Detects database operations without proper transaction boundaries.
Correctness High
Detects database operations spanning multiple queries without transaction boundaries.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without transactions:
* **Partial updates** — Some changes commit, others fail
* **Data inconsistency** — Concurrent reads see intermediate states
* **Lost writes** — Overwrites happen between read and update
* **Recovery issues** — Can’t rollback partial operations
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no transaction)
func transfer(db *sql.DB, from, to int, amount float64) error {
_, err := db.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
if err != nil {
return err
}
// If this fails, money is lost!
_, err = db.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
return err
}
```
```go
// ✅ After (with transaction)
func transfer(db *sql.DB, from, to int, amount float64) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
if err != nil {
return err
}
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
if err != nil {
return err
}
return tx.Commit()
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Multiple `db.Exec()` calls without `db.Begin()`
* Missing `tx.Commit()` or `tx.Rollback()`
* Related writes without transaction wrapper
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault wraps operations in transactions:
```go
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// ... operations
return tx.Commit()
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.sql\_injection](/docs/reference/rules/go/sql-injection/)
* [python.transaction\_boundary](/docs/reference/rules/python/transaction-boundary/)
# go.type_assertion_no_ok
> Detects type assertions without the ok check.
Stability Medium
Detects type assertions without the `ok` check.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Type assertions without the ok check panic on failure:
* **Runtime panics** — Application crashes on unexpected types
* **No graceful handling** — No way to recover or provide fallback
* **Hidden assumptions** — Type expectations not visible in code
* **Testing blind spots** — Works until it receives unexpected data
A single unchecked type assertion can bring down your entire service.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
value := data.(string) // Panics if not string
```
If `data` is anything other than a string, this panics.
```go
// ✅ After
value, ok := data.(string)
if !ok {
return errors.New("expected string")
}
```
Now you handle the error gracefully.
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Type assertions without the second `ok` value
* Interface type assertions without checking
* Type switches with fallthrough to assertion
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault converts single-value assertions to two-value form with error handling.
## Common Patterns
[Section titled “Common Patterns”](#common-patterns)
```go
// Two-value assertion (safe)
str, ok := v.(string)
if !ok {
return fmt.Errorf("expected string, got %T", v)
}
// Type switch (safest for multiple types)
switch v := data.(type) {
case string:
return processString(v)
case int:
return processInt(v)
default:
return fmt.Errorf("unexpected type: %T", v)
}
// Assertion with default value
str, ok := v.(string)
if !ok {
str = "default"
}
```
## When Single-Value Is Safe
[Section titled “When Single-Value Is Safe”](#when-single-value-is-safe)
```go
// After a type check (still not recommended)
if _, ok := v.(string); ok {
str := v.(string) // Safe but redundant
}
// Better: use type switch
if str, ok := v.(string); ok {
// Use str directly
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unchecked\_error](/docs/reference/rules/go/unchecked-error/)
* [rust.unsafe\_unwrap](/docs/reference/rules/rust/unsafe-unwrap/)
# go.unbounded_cache
> Detects in-memory caches without size limits.
Stability Medium
Detects in-memory caches without size limits that can grow indefinitely.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unbounded caches cause:
* **Memory exhaustion** — Cache grows until OOM
* **GC pressure** — Large heaps slow garbage collection
* **Unpredictable scaling** — Memory grows with traffic
* **Silent degradation** — No errors until crash
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (unbounded)
var cache = make(map[string]interface{})
func Get(key string) interface{} {
return cache[key]
}
func Set(key string, val interface{}) {
cache[key] = val // Grows forever!
}
```
```go
// ✅ After (bounded LRU)
import "github.com/hashicorp/golang-lru/v2"
var cache, _ = lru.New[string, interface{}](10000)
func Get(key string) (interface{}, bool) {
return cache.Get(key)
}
func Set(key string, val interface{}) {
cache.Add(key, val) // Evicts oldest when full
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Maps used as caches without eviction
* `sync.Map` without size limits
* Growing collections without bounds
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault suggests LRU cache libraries:
```go
import "github.com/hashicorp/golang-lru/v2"
// With TTL
import "github.com/hashicorp/golang-lru/v2/expirable"
cache := expirable.NewLRU[string, interface{}](
10000, // max entries
nil, // on evict callback
5 * time.Minute, // TTL
)
```
Tip
For distributed systems, consider Redis or Memcached instead of in-memory caches to share state across instances.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unbounded\_memory](/docs/reference/rules/go/unbounded-memory/)
* [python.unbounded\_cache](/docs/reference/rules/python/unbounded-cache/)
# go.unbounded_goroutines
> Detects goroutine spawning without bounds on concurrency.
Scalability High Causes Production Outages
Detects `go func()` calls without bounds on concurrent goroutines.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unbounded goroutine spawning exhausts resources:
* **Memory exhaustion** — Each goroutine uses \~2KB+ stack
* **CPU thrashing** — Too many goroutines competing for CPU
* **Downstream collapse** — Thousands of requests hit your database simultaneously
* **OOM crash** — Eventually the process is killed
Input-controlled fan-out is especially dangerous—attackers can DoS your service.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
for _, item := range items {
go process(item) // Spawns len(items) goroutines
}
```
If `items` has 100,000 elements, you spawn 100,000 goroutines simultaneously.
```go
// ✅ After
sem := make(chan struct{}, 100) // Limit to 100 concurrent
for _, item := range items {
sem <- struct{}{} // Acquire
go func(item Item) {
defer func() { <-sem }() // Release
process(item)
}(item)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `go func()` inside loops without semaphore
* Goroutine spawning driven by external input
* Missing worker pool patterns
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can wrap loop-spawned goroutines with semaphore-based limiting when the transformation is straightforward.
Note
Unfault detects goroutines spawned in loops without visible bounds. If you’re using errgroup.SetLimit() or a worker pool pattern elsewhere, Unfault may still flag the loop for review.
## Best Practices
[Section titled “Best Practices”](#best-practices)
```go
// Worker pool pattern
func processItems(items []Item, workers int) {
ch := make(chan Item, len(items))
var wg sync.WaitGroup
// Fixed number of workers
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for item := range ch {
process(item)
}
}()
}
// Feed items to workers
for _, item := range items {
ch <- item
}
close(ch)
wg.Wait()
}
// errgroup with limit
import "golang.org/x/sync/errgroup"
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(100)
for _, item := range items {
item := item
g.Go(func() error {
return process(ctx, item)
})
}
return g.Wait()
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.unbounded\_concurrency](/docs/reference/rules/python/unbounded-concurrency/)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
# go.unbounded_memory
> Detects operations that can consume unbounded memory.
Stability High
Detects operations that can consume unbounded memory, leading to OOM crashes.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unbounded memory operations cause:
* **OOM crashes** — Process killed by kernel
* **Pod evictions** — Kubernetes kills memory-heavy pods
* **Performance degradation** — GC pressure increases
* **Cascading failures** — Memory pressure affects other services
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (unbounded)
func readAll(r io.Reader) ([]byte, error) {
return io.ReadAll(r) // Could be gigabytes!
}
func collectAll(items <-chan Item) []Item {
var result []Item
for item := range items {
result = append(result, item) // Unbounded growth
}
return result
}
```
```go
// ✅ After (bounded)
func readLimited(r io.Reader, maxSize int64) ([]byte, error) {
return io.ReadAll(io.LimitReader(r, maxSize))
}
func collectBounded(items <-chan Item, maxItems int) []Item {
result := make([]Item, 0, min(maxItems, 1000))
for item := range items {
result = append(result, item)
if len(result) >= maxItems {
break
}
}
return result
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `io.ReadAll()` without size limits
* Unbounded slice appends in loops
* Growing maps without limits
* Collecting all results without pagination
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds size limits:
```go
// Limited reader
io.LimitReader(r, 10*1024*1024) // 10MB max
```
Tip
Set memory limits in Kubernetes with `resources.limits.memory` to prevent unbounded growth from affecting other pods.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unbounded\_cache](/docs/reference/rules/go/unbounded-cache/)
* [go.large\_response\_memory](/docs/reference/rules/go/large-response-memory/)
* [python.unbounded\_memory](/docs/reference/rules/python/unbounded-memory/)
# go.unbounded_retry
> Detects retry loops without proper bounds or backoff.
Stability High
Detects retry patterns that don’t have proper bounds, which can cause infinite loops on permanent failures.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unbounded retries cause:
* **Infinite loops** — Permanent failures never stop retrying
* **Resource exhaustion** — CPU and connections consumed by retries
* **DoS on dependencies** — Overwhelming already-struggling services
* **Extended outages** — Retries prevent recovery
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (unbounded)
func fetchData() error {
for {
if err := callAPI(); err == nil {
return nil
}
time.Sleep(time.Second)
}
}
```
```go
// ✅ After (bounded with backoff)
func fetchData() error {
maxRetries := 5
backoff := time.Second
for i := 0; i < maxRetries; i++ {
if err := callAPI(); err == nil {
return nil
}
time.Sleep(backoff)
backoff *= 2 // Exponential backoff
if backoff > time.Minute {
backoff = time.Minute
}
}
return errors.New("max retries exceeded")
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `for {}` loops with retry patterns
* Missing max retry count
* Missing backoff delays
* Retries without jitter
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates patches using retry libraries:
```go
import "github.com/cenkalti/backoff/v4"
func fetchData() error {
operation := func() error {
return callAPI()
}
b := backoff.NewExponentialBackOff()
b.MaxElapsedTime = 2 * time.Minute
b.MaxInterval = 30 * time.Second
return backoff.Retry(operation, b)
}
```
Tip
Add jitter to backoff to prevent thundering herd when multiple clients retry simultaneously.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.circuit\_breaker](/docs/reference/rules/go/circuit-breaker/)
* [go.http\_retry](/docs/reference/rules/go/http-retry/)
* [python.unbounded\_retry](/docs/reference/rules/python/unbounded-retry/)
# go.uncancelled_context
> Detects context.WithCancel/WithTimeout without cancellation.
Stability Medium
Detects `context.WithCancel()` and `context.WithTimeout()` without calling the cancel function, leaking resources.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Uncancelled contexts cause:
* **Memory leaks** — Context goroutines never terminate
* **Resource exhaustion** — Over time, leaks accumulate
* **Timer leaks** — WithTimeout/WithDeadline leak timers
* **Goroutine leaks** — Background goroutines never stop
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (cancel never called)
func process() {
ctx, _ := context.WithTimeout(context.Background(), time.Second)
doWork(ctx)
// Timer goroutine leaked!
}
```
```go
// ✅ After (cancel called)
func process() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() // Always call cancel!
doWork(ctx)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `context.WithCancel()` with unused cancel func
* `context.WithTimeout()` without defer cancel
* `context.WithDeadline()` without cleanup
* Ignored cancel functions (`_`)
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds `defer cancel()`:
```go
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
```
Tip
Always call cancel even if the operation completes successfully. It releases resources immediately rather than waiting for timeout.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.context\_background](/docs/reference/rules/go/context-background/)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
# go.unchecked_error
> Detects Go code that ignores error return values.
Correctness Medium
Detects Go code that ignores error return values.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Go’s error handling philosophy requires explicit handling:
* **Silent failures** — Operations fail but code continues as if successful
* **Data corruption** — Writes fail, reads return garbage, code proceeds
* **Debugging nightmare** — Errors surface far from their origin
* **Unexpected panics** — Nil results used without checking
Go makes errors explicit for a reason. Ignoring them defeats the language’s safety design.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
os.ReadFile("config.json") // Error ignored
json.Unmarshal(data, &config) // Error ignored
```
If the file doesn’t exist or JSON is invalid, you’ll get mysterious failures later.
```go
// ✅ After
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("reading config: %w", err)
}
if err := json.Unmarshal(data, &config); err != nil {
return fmt.Errorf("parsing config: %w", err)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Function calls with error return values that are discarded
* `_` used to explicitly ignore errors
* Error variables declared but never checked
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds error handling with appropriate error wrapping.
## Common Patterns
[Section titled “Common Patterns”](#common-patterns)
```go
// Return early on error
data, err := fetch()
if err != nil {
return nil, err
}
// Wrap with context
if err := save(data); err != nil {
return fmt.Errorf("saving data: %w", err)
}
// Log and continue (when recovery is possible)
if err := sendMetric(m); err != nil {
log.Warn().Err(err).Msg("failed to send metric")
// Continue - metrics are not critical
}
// Explicit ignore (rare, document why)
_ = conn.Close() // Best effort, already handling error
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [rust.ignored\_result](/docs/reference/rules/rust/ignored-result/)
* [typescript.empty\_catch](/docs/reference/rules/typescript/empty-catch/)
# go.unhandled_error_goroutine
> Detects errors not handled in goroutines.
Stability High
Detects errors not handled in goroutines.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unhandled errors in goroutines:
* **Silent failures** — Errors are lost without logging
* **Debugging difficulty** — No trace of what went wrong
* **Inconsistent state** — Failed operations go unnoticed
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (error silently ignored)
go func() {
result, err := processData(data)
if err != nil {
return // Error is lost!
}
// use result
}()
```
```go
// ✅ After (error properly handled)
go func() {
result, err := processData(data)
if err != nil {
log.Printf("processData failed: %v", err)
errorChan <- err
return
}
resultChan <- result
}()
```
```go
// ✅ Alternative (with error group)
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return processData(ctx, data)
})
if err := g.Wait(); err != nil {
log.Printf("goroutine failed: %v", err)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Error returns ignored in goroutines
* Missing error logging in goroutines
* Goroutines swallowing errors silently
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add error logging or channel-based error propagation patterns.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unchecked\_error](/docs/reference/rules/go/unchecked-error/)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
# go.unsafe_template
> Detects unsafe HTML template usage that can lead to XSS vulnerabilities.
Security Critical
Detects unsafe HTML template usage that can lead to cross-site scripting (XSS) vulnerabilities.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unsafe templates enable:
* **XSS attacks** — Attackers inject malicious scripts
* **Session hijacking** — Stolen cookies and tokens
* **Data theft** — Access to sensitive page content
* **Malware distribution** — Redirects to malicious sites
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (unsafe - uses text/template)
import "text/template"
func handler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
t := template.New("page")
t.Parse(`Hello {{.}}
`)
t.Execute(w, name) // XSS if name contains