Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 0 additions & 91 deletions .github/workflows/ci.yml

This file was deleted.

52 changes: 2 additions & 50 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
name: Release

on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]
push:
tags: ["v*"]

jobs:
release:
name: Release
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
permissions:
contents: write
steps:
Expand All @@ -22,52 +19,7 @@ jobs:
with:
go-version-file: go.mod

- name: Get latest tag
id: get_tag
run: |
git fetch --tags

LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | head -n 1)
if [ -z "$LATEST_TAG" ]; then
LATEST_TAG="v0.0.0"
fi
echo "latest=$LATEST_TAG" >> $GITHUB_OUTPUT

VERSION=${LATEST_TAG#v}
MAJOR=$(echo $VERSION | cut -d. -f1)
MINOR=$(echo $VERSION | cut -d. -f2)
PATCH=$(echo $VERSION | cut -d. -f3)

NEW_PATCH=$((PATCH + 1))
NEW_TAG="v${MAJOR}.${MINOR}.${NEW_PATCH}"

if [ "$LATEST_TAG" = "v0.0.0" ]; then
NEW_TAG="v0.1.0"
fi

echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT
echo "Latest: $LATEST_TAG -> New: $NEW_TAG"

- name: Create tag
id: create_tag
env:
NEW_TAG: ${{ steps.get_tag.outputs.new_tag }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

if git ls-remote --tags origin | grep -q "refs/tags/$NEW_TAG$"; then
echo "Tag $NEW_TAG already exists on remote, skipping release"
echo "skip=true" >> $GITHUB_OUTPUT
exit 0
fi

git tag "$NEW_TAG"
git push origin "$NEW_TAG"
echo "skip=false" >> $GITHUB_OUTPUT

- name: Run GoReleaser
if: steps.create_tag.outputs.skip != 'true'
uses: goreleaser/goreleaser-action@v6
with:
version: latest
Expand Down
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# Changelog

## feat/config-package-go126
## feat/bodydecoder-registry

Protobuf and gRPC-Web bodies are now decoded into human-readable text in the
detail view (#26). A new `--proto-path` flag loads `.proto` files for named
message decoding; without it, raw wire-format decoding is always available. The
`BodyDecoderRegistry` interface threads through the TUI port layer, and a
`renderOpts` refactor replaces six positional render arguments with a single
struct. Protobuf/gRPC MIME types are removed from the binary blocklist so they
flow through the decoder pipeline. A gRPC-Web frame parser fix resolves a gosec
G115 integer overflow, and a new E2E test exercises a full Connect RPC
round-trip with proto decoding.

## [feat/config-package-go126](https://github.com/kostyay/httpmon/pull/22) - 2026-02-16

Persistent configuration via `~/.httpmon/config.json` replaces pure CLI-flag
defaults (#22). A new `internal/config` package handles Load/Save with automatic
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all fmt lint test e2e build clean security release
.PHONY: all fmt lint test e2e build clean security release generate-proto

all: lint test build

Expand Down Expand Up @@ -31,6 +31,14 @@ release: lint test security
git tag "$$TAG" && git push origin "$$TAG" && \
GITHUB_TOKEN=$$(gh auth token) goreleaser release --clean

# Requires: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest
# go install github.com/bufbuild/buf/cmd/buf@latest
generate-proto:
buf generate \
--template '{"version":"v2","plugins":[{"remote":"buf.build/protocolbuffers/go","out":"internal/e2e/testpb","opt":"paths=source_relative"},{"remote":"buf.build/connectrpc/go","out":"internal/e2e/testpb","opt":"paths=source_relative"}]}' \
internal/bodydecoder/testdata/test.proto

security:
@echo "==> gosec"
@command -v gosec >/dev/null 2>&1 && gosec ./... || echo "gosec not installed (go install github.com/securego/gosec/v2/cmd/gosec@latest)"
Expand Down
31 changes: 31 additions & 0 deletions cmd/httpmon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

tea "charm.land/bubbletea/v2"

"github.com/kostyay/httpmon/internal/bodydecoder"
"github.com/kostyay/httpmon/internal/breakpoint"
"github.com/kostyay/httpmon/internal/certutil"
"github.com/kostyay/httpmon/internal/config"
Expand All @@ -27,6 +28,15 @@ import (

var version = "dev"

// stringSlice implements flag.Value for repeatable string flags.
type stringSlice []string

func (s *stringSlice) String() string { return strings.Join(*s, ",") }
func (s *stringSlice) Set(v string) error {
*s = append(*s, v)
return nil
}

func main() {
flag.Int("port", 8080, "proxy listen port")
dataDir := flag.String("data-dir", defaultDataDir(), "data directory for CA certs")
Expand All @@ -40,6 +50,8 @@ func main() {
flag.Bool("mcp", false, "start MCP server on default addr (127.0.0.1:9551)")
flag.String("mcp-addr", "", "MCP server listen address (implies --mcp)")
mcpTokenFlag := flag.Bool("mcp-token", false, "print MCP bearer token and exit")
var protoPaths stringSlice
flag.Var(&protoPaths, "proto-path", "path to .proto file or directory (repeatable)")
flag.Parse()

if *showVersion {
Expand All @@ -53,6 +65,11 @@ func main() {
}
config.ApplyFlags(cfg, flag.Visit)

// Merge proto paths: config first, CLI appended.
if len(protoPaths) > 0 {
cfg.ProtoPaths = append(cfg.ProtoPaths, protoPaths...)
}

// --mcp-addr implies --mcp.
flag.Visit(func(f *flag.Flag) {
if f.Name == "mcp-addr" {
Expand Down Expand Up @@ -146,6 +163,19 @@ func main() {
fmt.Fprintf(os.Stderr, "MCP server listening on %s (token: %s)\n", mcpSrv.Addr(), cfg.MCPToken)
}

// Build body decoder registry for protobuf/gRPC decoding.
// Even without .proto files, raw wire-format decoding is available.
protoDec := &bodydecoder.RawProtobufDecoder{}
if len(cfg.ProtoPaths) > 0 {
protoReg, protoErrs := bodydecoder.LoadProtoFiles(cfg.ProtoPaths)
for _, e := range protoErrs {
fmt.Fprintf(os.Stderr, "proto: %v\n", e)
}
protoDec.ProtoReg = protoReg
}
grpcDec := &bodydecoder.GRPCWebDecoder{Proto: protoDec}
decoderReg := bodydecoder.NewRegistry(grpcDec, protoDec)

tuiCfg := tui.AppConfig{
Store: s,
Proxy: p,
Expand All @@ -154,6 +184,7 @@ func main() {
Throttle: p,
Breakpoints: bpCtrl,
DataDir: *dataDir,
BodyDecoder: decoderReg,
}
if mcpSrv != nil {
tuiCfg.MCP = mcpSrv
Expand Down
53 changes: 53 additions & 0 deletions docs/plans/2026-02-24-grpc-e2e-tests-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# gRPC-Web E2E Tests Design

## Goal

End-to-end tests for gRPC/protobuf body decoding. Real gRPC-Web server (Connect) → httpmon proxy → store → TUI decode + render.

## Dependencies

- `connectrpc.com/connect` — gRPC-Web server + client
- `protoc-gen-go`, `protoc-gen-connect-go` — code generation (dev tools, not Go deps)

## Generated Code

Source: `internal/bodydecoder/testdata/test.proto` (Greeter service, HelloRequest/HelloReply).
Output: `internal/e2e/testpb/` (committed).

```
internal/e2e/testpb/
test.pb.go
testpbconnect/
test.connect.go
```

## Harness Extension

Extend `newHarness` with variadic `harnessOpt`:

```go
type harnessOpt func(*harnessConfig)
func withBodyDecoder(reg *bodydecoder.Registry) harnessOpt
```

Existing tests unchanged (no opts passed).

`grpcHarness` wraps base harness + Connect Greeter client routed through the proxy with `WithGRPCWeb()`.

## Test Scenarios (`grpc_test.go`)

1. **TestGRPCWebDecodeResponse** — SayHello, response tab shows named JSON fields
2. **TestGRPCWebDecodeRequest** — request tab shows named JSON fields
3. **TestGRPCWebRawToggle** — `r` key toggles between decoded and raw
4. **TestGRPCWebWithoutProtoFiles** — no proto paths → field-number JSON ("1", "2")
5. **TestGRPCWebDecodeError** — garbage bytes with grpc-web content type → status bar error
6. **TestGRPCWebNonProtoUnchanged** — regular JSON GET still works normally

## Makefile

```makefile
generate-proto:
protoc ...
```

Not wired into `make all`.
Loading