Skip to content

I-invincib1e/Httli

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

18 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Httli Banner

Httli

A zero-dependency, colorful command-line HTTP client and developer workflow platform.

License: MIT Go CI PRs Welcome

Installation β€’ Quick Start β€’ All Flags β€’ Collections β€’ Environments β€’ History β€’ Scripting & CI β€’ How It Works


⚑ What is Httli?

Httli is a fast, terminal-native HTTP client built in Go with zero external runtime dependencies. It goes beyond curl to give you a complete developer workflow β€” saved requests, environments, history, batch execution, and JSON extraction β€” all from your terminal.

# Fire a quick request
httli -u https://api.github.com/users/octocat

# POST with auth and body
httli -m POST -u https://api.example.com/login \
  -d '{"user":"admin","pass":"secret"}' \
  -b mytoken

# Save it, then run it in any environment
httli collection save auth/login -m POST -u {{BASE_URL}}/login -d '{"user":"admin"}'
httli collection run auth/login --env prod

# Run a whole test suite in CI
httli collection run-all auth/ --fail-fast --timeout 10s --format json

πŸ“¦ Installation

Build from source

git clone https://github.com/I-invincib1e/Httli.git
cd Httli
go build -o httli ./cmd/httli/main.go

Build with version injection:

go build -ldflags "-X github.com/I-invincib1e/httli/cmd.Version=1.1.0" -o httli ./cmd/httli/main.go

Add to PATH

# Linux / macOS
sudo mv httli /usr/local/bin/

# Windows (Admin PowerShell)
Move-Item httli.exe C:\Windows\System32\

Shell Autocomplete

source <(httli completion bash)        # Bash
source <(httli completion zsh)         # Zsh
httli completion powershell | Invoke-Expression  # PowerShell

πŸš€ Quick Start

Send a request

# GET (default)
httli -u https://jsonplaceholder.typicode.com/posts/1

# POST with JSON body
httli -m POST -u https://api.example.com/users \
  -d '{"name":"Alice"}' \
  -H "Content-Type:application/json"

# With bearer token
httli -u https://api.example.com/me -b eyJhbGc...

# With basic auth
httli -u https://api.example.com/secure -a user:pass

# Read body from file
httli -m POST -u https://api.example.com/data -d @payload.json

# Read body from stdin
echo '{"key":"val"}' | httli -m POST -u https://api.example.com/data -d @-

Command overview

Command Description
httli [flags] Quick mode β€” fires a request directly
httli request send [flags] Explicit request with all options
httli collection save <name> [flags] Save a request to your collection
httli collection update <name> [flags] Update an existing saved request
httli collection run <name> [flags] Run a saved request
httli collection run-all [prefix] [flags] Run all saved requests (batch)
httli collection list List all saved requests
httli collection describe <name> <text> Annotate a saved request
httli collection export <file> Export collections to JSON
httli collection import <file> Import collections from JSON
httli history View request history (last 50)
httli history show <n> Inspect history entry details
httli history clear Clear all history
httli rerun <n> [flags] Re-execute a history entry
httli env list Show loaded environment variables
httli completion <shell> Generate shell autocomplete
httli version Print version

πŸ”§ All Flags

Request flags

Short Long Default Description
-m --method GET HTTP method
-u --url β€” URL to request (required)
-d --data β€” Request body β€” JSON string, @- for stdin, @file.json for file
-f --file β€” Read body from file (alternative to @file)
-H --header β€” Headers as Key:Value,Key2:Value2
-b --bearer β€” Bearer token (sets Authorization: Bearer <token>)
-a --auth β€” Basic auth as user:pass (sets Authorization: Basic ...)
-t --timeout 30s Request timeout (Go duration: 5s, 1m30s)
-L --follow false Follow HTTP redirects
-e --env β€” Load .env.<name> in addition to .env and .env.local

Output flags

Short Long Description
-o --output Save response body to file (always runs, even with --raw)
-x --extract Extract a field from JSON response using dot notation (.data.token, .items[0].id)
--format json Output structured JSON (for piping to jq)
--raw Print raw body only β€” no colors, headers, or formatting
-s --status-only Print only the numeric status code
-q --quiet Print only the response body
-S --silent Suppress all output β€” exit code only
-v --verbose Show request/response headers

Control flags

Short Long Description
-F --fail Exit 22 on HTTP 4xx/5xx (curl convention)
--fail-fast Stop run-all batch on first error
-r --retry Number of retries on network error or 5xx
--retry-delay Seconds between retries (default: 2)
--retry-backoff Opt-in exponential backoff β€” delay doubles each attempt, capped at 30s
--dry-run Print resolved request without making a network call
--ignore-missing-env Don't error on unresolved {{VAR}} placeholders

πŸ“ Collections

Collections let you save, name, and replay HTTP requests β€” with full fidelity, including auth, timeouts, and headers.

Save and run

# Save a request (fails if name already exists)
httli collection save auth/login \
  -m POST \
  -u {{BASE_URL}}/auth/login \
  -d '{"user":"admin"}' \
  -b mytoken \
  --timeout 15s \
  --retry 2

# Add a description (shown in list)
httli collection describe auth/login "Authenticate and get session token"

# Update an existing request
httli collection update auth/login -b newtoken

# Run it
httli collection run auth/login --env prod

# Run with runtime overrides
httli collection run auth/login --timeout 5s --format json --verbose

List and inspect

httli collection list
Saved Requests:

  Ungrouped/
    - ping [GET https://api.example.com/ping]

  auth/
    - login [POST https://{{BASE_URL}}/auth/login]  # Authenticate and get session token
    - refresh [POST https://{{BASE_URL}}/auth/refresh]

What gets saved

Every field is persisted β€” nothing is silently dropped:

Field Saved?
Method, URL βœ…
Headers βœ…
Body βœ…
Bearer token βœ…
Basic auth βœ…
Timeout βœ… (stored as "15s")
Follow redirects βœ…
Retry count & delay βœ…
Description βœ…
Created / Updated timestamps βœ…

Namespace grouping

Use / in the name to group requests:

httli collection save api/users/list   -m GET  -u {{BASE_URL}}/users
httli collection save api/users/create -m POST -u {{BASE_URL}}/users -d '{"name":"Bob"}'
httli collection save api/products     -m GET  -u {{BASE_URL}}/products

Batch execution (run-all)

# Run everything
httli collection run-all

# Run only the auth/ group
httli collection run-all auth/

# Stop immediately on first failure
httli collection run-all --fail-fast

# Apply runtime overrides to all
httli collection run-all --timeout 10s --retry 1 --format json

State chaining between requests β€” run-all automatically sets env vars after each step:

Variable Value
HTTLI_LAST_STATUS HTTP status code, e.g. 200
HTTLI_LAST_BODY_PATH Absolute path to temp file with raw body
HTTLI_LAST_JSON Raw JSON body string (if ≀ 32KB, valid JSON)

Use {{HTTLI_LAST_JSON}} in a subsequent request body to chain results:

httli collection save api/order -m POST -u {{BASE_URL}}/order \
  -d '{"token":"{{HTTLI_LAST_JSON}}"}'

Export and Import

# Export all collections to file
httli collection export my-api.json

# Import (3 modes)
httli collection import my-api.json              # merge (default) β€” add new, skip conflicts
httli collection import my-api.json --overwrite  # replace all existing with imported

Tip: Create a .httli/ directory in your project root to scope collections to that project. Commit .httli/ to share collections with your team alongside your code.


🌍 Environments

Use {{VAR}} placeholders anywhere β€” URLs, headers, body, auth β€” and substitute them with .env files.

.env file format

BASE_URL=https://api.example.com
API_TOKEN=secret123
DB_USER=admin

Loading order (highest priority last)

.env            β†’ base defaults (committed, shared)
.env.local      β†’ local overrides (git-ignored, personal)
.env.<name>     β†’ activated via --env flag (e.g., .env.prod)

Safe for CI/CD: Httli never overwrites an environment variable that already exists in the shell. Your CI platform's secrets always win.

Usage

# Use base .env only
httli -u {{BASE_URL}}/users -b {{API_TOKEN}}

# Activate a specific environment
httli collection run auth/login --env prod   # loads .env + .env.local + .env.prod
httli collection run auth/login --env staging

View loaded variables

httli env list
Active Environment: prod

Loaded files (in order):
  βœ“ .env
  βœ“ .env.local
  βœ“ .env.prod
  βœ— (no other files)

Chaining Variables:
  HTTLI_LAST_STATUS = 200

All Environment Variables:
  API_TOKEN = secret123
  BASE_URL = https://api.example.com

Missing variable behavior

# Strict mode (default) β€” errors on unresolved {{VAR}}
httli -u {{MISSING_VAR}}/api
# Error: environment variable 'MISSING_VAR' not found

# Permissive mode β€” leaves placeholder as-is
httli -u {{MISSING_VAR}}/api --ignore-missing-env

πŸ“Š History

Every executed request is automatically recorded. The last 50 entries are kept.

# View history with status and duration
httli history
Request History:
  [1] βœ“ GET https://api.example.com/users β†’ 200 (124ms)  2026-04-01T01:00:00+05:30
  [2] βœ— POST https://api.example.com/login β†’ 401 (89ms)  2026-04-01T01:01:00+05:30
  [3] βœ“ GET https://jsonplaceholder.typicode.com/posts/1 β†’ 200 (287ms)  2026-04-01T01:02:00+05:30
# Inspect a full entry (method, URL, headers, body, auth, duration)
httli history show 1

# Re-run entry #1 with overrides β€” full fidelity (headers, body, auth restored)
httli rerun 1
httli rerun 1 --timeout 5s --format json --verbose

# Clear all history
httli history clear

Full-fidelity rerun: rerun restores the complete original request β€” method, URL, headers, body, bearer token, and basic auth β€” not just the URL.


πŸ€– Scripting & CI

Httli is designed for headless use in scripts and pipelines.

Exit codes

Code Meaning
0 Success
1 Usage error / network error
22 HTTP 4xx/5xx (when --fail is set)

JSON output pipeline

# Structured JSON output to pipe to jq
httli -u https://api.example.com/users --format json | jq '.body.users[0].name'

# Native extraction (no jq needed)
httli -u https://api.example.com/data --extract .items[0].id

# Dot-notation supports arrays
httli -u https://api.example.com/data --extract .data.users[0].email

Stdin as request body

cat payload.json | httli -m POST -u https://api.example.com/ingest -d @-

Silent mode for scripts

# Script only cares about exit code
httli -u https://api.example.com/health --fail --silent
if [ $? -eq 0 ]; then echo "API is up"; fi

# Just print status code
STATUS=$(httli -u https://api.example.com/health --status-only)
echo "Got: $STATUS"

Dry run

# See the fully resolved request (env vars substituted) without firing it
httli -m POST -u {{BASE_URL}}/login -d '{"user":"admin"}' --env prod --dry-run

Retry strategies

# Fixed delay retry (default)
httli -u https://api.example.com/data --retry 3 --retry-delay 5

# Exponential backoff (2s, 4s, 8s... capped at 30s)
httli -u https://api.example.com/data --retry 4 --retry-delay 2 --retry-backoff

# Verbose retry output to stderr (won't pollute stdout)
httli -u https://api.example.com/data --retry 2 --verbose
# β†’ Retry 1/2 in 2s (HTTP 503)
# β†’ Retry 2/2 in 2s (HTTP 503)

CI/CD example

#!/bin/bash
# Run full API test suite, fail on first error, timeout per request, exit 22 on any 4xx/5xx
httli collection run-all smoke-tests/ \
  --env ci \
  --fail-fast \
  --fail \
  --timeout 10s \
  --retry 1 \
  --format json \
  --silent

Save response to file

# --output works with ALL display modes (raw, silent, status-only)
httli -u https://api.example.com/report --output report.json --silent
httli -u https://api.example.com/report --output report.json --raw

πŸ— How It Works

Architecture

httli/
β”œβ”€β”€ cmd/                        # Command layer (CLI entry points)
β”‚   β”œβ”€β”€ httli/main.go           # Binary entry point
β”‚   β”œβ”€β”€ root.go                 # Command dispatcher (recursive, N-depth)
β”‚   β”œβ”€β”€ request.go              # httli [flags] / request send
β”‚   β”œβ”€β”€ collection.go           # collection save/update/run/list/describe/export/import
β”‚   β”œβ”€β”€ collection_runall.go    # collection run-all (batch execution)
β”‚   β”œβ”€β”€ history.go              # history / rerun
β”‚   β”œβ”€β”€ env.go                  # env list
β”‚   β”œβ”€β”€ completion.go           # shell autocomplete
β”‚   └── version.go              # version
β”œβ”€β”€ internal/
β”‚   β”œβ”€β”€ client/                 # HTTP execution engine
β”‚   β”‚   └── client.go           # ExecuteRequest, retry loop, backoff, auth
β”‚   β”œβ”€β”€ config/                 # Flag parsing and env var system
β”‚   β”‚   β”œβ”€β”€ config.go           # Config struct, ParseFlags, ApplyOverrides
β”‚   β”‚   β”œβ”€β”€ env.go              # LoadEnv, Interpolate ({{VAR}} system)
β”‚   β”‚   └── global.go           # Global config (default env, project-local detection)
β”‚   β”œβ”€β”€ collections/            # Collection storage (save/get/list/import/export)
β”‚   β”‚   └── collections.go
β”‚   β”œβ”€β”€ history/                # Request history (record/load/list/show/clear)
β”‚   β”‚   └── history.go
β”‚   β”œβ”€β”€ output/                 # Response rendering (pretty/raw/json/extract)
β”‚   β”‚   └── output.go
β”‚   β”œβ”€β”€ storage/                # Path resolution (project-local vs global)
β”‚   β”‚   └── resolver.go
β”‚   └── styles/                 # Terminal color styles (lipgloss)
β”‚       └── styles.go

Request lifecycle

httli -m POST -u {{BASE_URL}}/login -d '{"user":"admin"}' --env prod
         β”‚
         β–Ό
1. ParseFlags()          β€” parse all CLI flags into Config struct
         β”‚
         β–Ό
2. LoadEnv(prod)         β€” load .env, .env.local, .env.prod
         β”‚                 (never overwrites existing shell vars)
         β–Ό
3. InterpolateAll()      β€” replace {{BASE_URL}} with env var value
         β”‚                 (errors on missing vars unless --ignore-missing-env)
         β–Ό
4. Validate()            β€” ensure URL is present, format is valid
         β”‚
         β–Ό
5. [dry-run check]       β€” if --dry-run, print request and exit
         β”‚
         β–Ό
6. ExecuteRequest()      β€” make HTTP call
         β”‚                 add Auth headers (bearer β†’ Authorization: Bearer ...)
         β”‚                 JSON body validation
         β”‚                 retry loop (with optional exponential backoff)
         β”‚                 5xx body always captured before retry
         β”‚                 verbose retry logging to stderr
         β–Ό
7. history.Record()      β€” save full request + response metadata to history
         β”‚
         β–Ό
8. DisplayResponse()     β€” render output:
                           --output saves file first (orthogonal to display mode)
                           then: extract β†’ format json β†’ raw β†’ status-only β†’ quiet β†’ full pretty

Storage locations

Httli uses project-local storage when a .httli/ directory is detected in the current working directory. Otherwise it falls back to a global directory.

Platform Global path
Windows %APPDATA%\httli\
macOS ~/Library/Application Support/httli/
Linux ~/.config/httli/
File Contents
collections.json All saved requests
history.json Last 50 executed requests

Flag system internals

  • ParseFlags() uses flag.ContinueOnError β€” unknown flags return errors instead of calling os.Exit
  • All flags have both short (-m) and long (--method) forms registered in the same FlagSet
  • InterpolateAll() is not called in ParseFlags() β€” callers invoke it explicitly, preventing double-substitution when collection run or rerun call it after loading the saved config
  • ApplyOverrides(runCfg) merges runtime CLI flags onto a loaded collection config β€” timeout is only overridden if the user explicitly set a non-default value

The {{VAR}} interpolation system

Placeholders are matched by the regex \{\{\s*([A-Za-z0-9_]+)\s*\}\} (compiled once at package level). The regex supports optional whitespace ({{ VAR }} or {{VAR}}). Interpolation runs across: URL, body, bearer token, basic auth, and all header values.

Retry logic

attempt 1  β†’ request β†’ 5xx or network error β†’ read body β†’ save lastResp
attempt 2  β†’ sleep(delay) β†’ request β†’ ...
attempt N  β†’ sleep(delay) β†’ request β†’ 5xx β†’ return lastResp (NOT an error)

# with --retry-backoff:
delay formula: baseDelay Γ— 2^attempt, capped at 30s
attempt 1: 2s, attempt 2: 4s, attempt 3: 8s, attempt 4: 16s, attempt 5+: 30s

On the final attempt, the 5xx Response is returned (not an error) so callers can inspect resp.StatusCode and resp.Body β€” enabling --fail exit code logic and run-all summary tables to work correctly with server error details.


🀝 Contributing

Contributions are welcome! See CONTRIBUTING.md for guidelines.

Running tests

go test ./...
go vet ./...

Project conventions

  • All new features belong in internal/ β€” keep cmd/ as a thin dispatch layer
  • ParseFlags() must not call InterpolateAll() β€” callers do this explicitly
  • New config fields go in Config struct with both short+long flag registration
  • Exported functions in internal/ packages take *config.Config, not individual params

πŸ“„ License

MIT License β€” see LICENSE for details.

About

A colorful, lightweight command-line HTTP client similar to Postman. Built with Go for fast performance and beautiful terminal output.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Languages