From f59d9945bbd3d5a57862beb963e7b20d94fa7f03 Mon Sep 17 00:00:00 2001
From: Jon Gallant <2163001+jongio@users.noreply.github.com>
Date: Fri, 10 Jul 2026 10:38:13 -0700
Subject: [PATCH] Add custom HTTP request command
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
README.md | 2 +-
cli/docs/cli-reference.md | 19 +++++++++
cli/src/internal/cmd/root.go | 32 +++++++++++++++
cli/src/internal/cmd/root_test.go | 49 ++++++++++++++++++++++-
cli/src/internal/skills/azd-rest/SKILL.md | 2 +
5 files changed, 102 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index c2cac09..cde76ec 100644
--- a/README.md
+++ b/README.md
@@ -70,7 +70,7 @@ Built-in Model Context Protocol server for AI agent integration. Copilot and oth
### 🔄 All HTTP Methods
-GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS with JSON body support from inline data or files.
+GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, and custom methods through `request ` with JSON body support from inline data or files.
### 📊 Verbose Diagnostics
Request/response details, traceparent injection for distributed tracing, and redacted sensitive headers in logs.
diff --git a/cli/docs/cli-reference.md b/cli/docs/cli-reference.md
index b7a5f27..79dc1c1 100644
--- a/cli/docs/cli-reference.md
+++ b/cli/docs/cli-reference.md
@@ -37,6 +37,7 @@ azd rest version
| `delete` | Execute a DELETE request |
| `head` | Execute a HEAD request |
| `options` | Execute an OPTIONS request |
+| `request` | Execute a request with a custom HTTP method |
| `scope` | Preview the detected OAuth scope and auth mode for a URL |
| `version` | Display the extension version |
@@ -165,6 +166,24 @@ azd rest options [flags]
azd rest options https://api.example.com/resource
```
+### `azd rest request `
+
+Execute a request with a custom HTTP method while keeping the same auth, retry, formatting, and safety behavior as the named method commands.
+
+**Usage:**
+```bash
+azd rest request [flags]
+```
+
+**Examples:**
+```bash
+# Send an uncommon method name
+azd rest request PURGE https://management.azure.com/... --api-version 2024-01-01
+
+# Lowercase input is normalized before the request is sent
+azd rest request merge https://api.example.com/resource --no-auth
+```
+
---
## Global Flags
diff --git a/cli/src/internal/cmd/root.go b/cli/src/internal/cmd/root.go
index 7c1a516..8a807cd 100644
--- a/cli/src/internal/cmd/root.go
+++ b/cli/src/internal/cmd/root.go
@@ -5,6 +5,7 @@ import (
"context"
"fmt"
"os"
+ "strings"
"time"
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
@@ -118,6 +119,36 @@ func NewHeadCommand() *cobra.Command { return newHTTPMethodCommand(httpMethods[5
// NewOptionsCommand returns the OPTIONS subcommand.
func NewOptionsCommand() *cobra.Command { return newHTTPMethodCommand(httpMethods[6]) }
+// NewRequestCommand returns the generic request subcommand for uncommon HTTP methods.
+func NewRequestCommand() *cobra.Command {
+ return &cobra.Command{
+ Use: "request ",
+ Short: "Execute a request with a custom HTTP method",
+ Long: `Execute a request with any HTTP method while reusing the same authentication,
+retry, formatting, and safety behavior as the named method commands.
+
+Examples:
+ azd rest request PURGE https://management.azure.com/... --api-version 2024-01-01
+ azd rest request merge https://api.example.com/resource --no-auth`,
+ Args: cobra.ExactArgs(2),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ method, err := normalizeRequestMethod(args[0])
+ if err != nil {
+ return err
+ }
+ return executeRequest(cmd, method, args[1])
+ },
+ }
+}
+
+func normalizeRequestMethod(method string) (string, error) {
+ normalized := strings.ToUpper(strings.TrimSpace(method))
+ if normalized == "" {
+ return "", fmt.Errorf("method is required")
+ }
+ return normalized, nil
+}
+
// NewRootCmd creates the root command for azd rest
func NewRootCmd() *cobra.Command {
rootCmd, _ := azdext.NewExtensionRootCommand(azdext.ExtensionCommandOptions{
@@ -235,6 +266,7 @@ Examples:
// Add non-HTTP-method subcommands
rootCmd.AddCommand(
+ NewRequestCommand(),
NewScopeCommand(),
azdext.NewVersionCommand("jongio.azd.rest", version.Version, &outputFormat),
azdext.NewMetadataCommand("1.0", "jongio.azd.rest", NewRootCmd),
diff --git a/cli/src/internal/cmd/root_test.go b/cli/src/internal/cmd/root_test.go
index a1268be..998b361 100644
--- a/cli/src/internal/cmd/root_test.go
+++ b/cli/src/internal/cmd/root_test.go
@@ -78,12 +78,59 @@ func TestNewRootCmd(t *testing.T) {
}
}
- expectedCommands := []string{"get", "post", "put", "patch", "delete", "head", "options", "scope", "version"}
+ expectedCommands := []string{"get", "post", "put", "patch", "delete", "head", "options", "request", "scope", "version"}
for _, expected := range expectedCommands {
assert.True(t, subcommandNames[expected], "Subcommand %s should be present", expected)
}
}
+func TestNormalizeRequestMethod(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want string
+ wantErr bool
+ }{
+ {name: "uppercase is unchanged", input: "PURGE", want: "PURGE"},
+ {name: "lowercase is uppercased", input: "merge", want: "MERGE"},
+ {name: "spaces are trimmed", input: " link ", want: "LINK"},
+ {name: "empty is rejected", input: " ", wantErr: true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := normalizeRequestMethod(tt.input)
+ if tt.wantErr {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func TestRequestCommandExecutesCustomMethod(t *testing.T) {
+ resetGlobalFlags()
+ var gotMethod string
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ gotMethod = r.Method
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(`{"ok":true}`))
+ }))
+ defer server.Close()
+
+ noAuth = true
+ outputFile = filepath.Join(t.TempDir(), "response.json")
+
+ cmd := NewRequestCommand()
+ cmd.SetArgs([]string{"purge", server.URL + "/cache"})
+
+ require.NoError(t, cmd.Execute())
+ assert.Equal(t, "PURGE", gotMethod)
+}
+
func TestNewRootCmd_SilentFlag(t *testing.T) {
resetGlobalFlags()
cmd := NewRootCmd()
diff --git a/cli/src/internal/skills/azd-rest/SKILL.md b/cli/src/internal/skills/azd-rest/SKILL.md
index edbafe3..a045c1d 100644
--- a/cli/src/internal/skills/azd-rest/SKILL.md
+++ b/cli/src/internal/skills/azd-rest/SKILL.md
@@ -29,6 +29,8 @@ azd rest [flags]
Supported HTTP methods: `get`, `post`, `put`, `patch`, `delete`, `head`, `options`
+Use `azd rest request ` for uncommon method names such as `PURGE`, `MERGE`, or `LINK`.
+
Use `azd rest scope ` to preview the detected OAuth scope and auth mode for a URL without sending a request.
## Flags
|