Skip to content
Open
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
162 changes: 128 additions & 34 deletions src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,45 @@ function csharpMethod(method: string): string {
}

function javaMethod(method: string): string {
return `HttpMethod.${method.toUpperCase()}`;
const map: Record<string, string> = {
GET: 'HttpMethod.GET',
POST: 'HttpMethod.POST',
PUT: 'HttpMethod.PUT',
DELETE: 'HttpMethod.DELETE',
PATCH: 'HttpMethod.PATCH',
};
return map[method.toUpperCase()] || `HttpMethod.valueOf("${method.toUpperCase()}")`;
}

function buildCurlSnippet(opts: {
method: string;
path: string;
pathParams?: Record<string, string>;
queryParams?: Record<string, string>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
body?: Record<string, any>;
streaming?: boolean;
}): string {
const { method, path, pathParams, queryParams, body, streaming } = opts;
let url = `$FGA_API_URL${path}`;
if (pathParams) {
for (const [k, v] of Object.entries(pathParams)) {
url = url.split(`{${k}}`).join(v);
}
}
if (queryParams && Object.keys(queryParams).length > 0) {
const qs = Object.entries(queryParams)
.map(([k, v]) => `${k}=${v}`)
.join('&');
url += `?${qs}`;
}
let code = streaming ? `curl -N -X ${method} '${url}' \\\n` : `curl -X ${method} '${url}' \\\n`;
code += ` -H 'Content-Type: application/json' \\\n`;
code += ` -H 'Authorization: Bearer $FGA_API_TOKEN'`;
if (body) {
code += ` \\\n -d '${JSON.stringify(body, null, 2)}'`;
}
return code;
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -268,8 +306,15 @@ ${parts.join('\n')}
return code;
}

case SupportedLanguage.CURL: {
let code = buildCurlSnippet({ method, path, pathParams, queryParams, body });
if (responseExample) {
code += `\n\n# Response: ${responseExample}`;
}
return code;
}
Comment on lines +309 to +315
Comment on lines +309 to +315
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Generated curl commands are malformed for common inputs (no URL-encoding, unsafe single-quote body wrapping).

Three concrete defects in the new CURL branch will cause users who copy-paste the snippet to hit broken requests:

  1. Path parameters are not URL-encoded. url.split(\{${k}}`).join(v)substitutes raw values, so anyvcontaining/, :, ?, #`, space, etc. produces a malformed URL.
  2. Query parameters are not URL-encoded. ${k}=${v} will silently break for values containing &, =, #, +, or spaces.
  3. Body wrapped in single quotes will break on apostrophes. -d '${JSON.stringify(body, null, 2)}' — if any JSON string value contains a single quote (e.g. a user name like O'Brien), the shell single-quote string is terminated early and the resulting command is invalid. This is realistic for example data shown in docs.
🛠️ Proposed fix
     case SupportedLanguage.CURL: {
       let url = `$FGA_API_URL${path}`;
       if (pathParams) {
         for (const [k, v] of Object.entries(pathParams)) {
-          url = url.split(`{${k}}`).join(v);
+          url = url.split(`{${k}}`).join(encodeURIComponent(v));
         }
       }
       if (queryParams && Object.keys(queryParams).length > 0) {
         const qs = Object.entries(queryParams)
-          .map(([k, v]) => `${k}=${v}`)
+          .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
           .join('&');
         url += `?${qs}`;
       }
       let code = `curl -X ${method} '${url}' \\\n`;
       code += `  -H 'Content-Type: application/json' \\\n`;
       code += `  -H 'Authorization: Bearer $FGA_BEARER_TOKEN'`;
       if (body) {
-        code += ` \\\n  -d '${JSON.stringify(body, null, 2)}'`;
+        // Escape single quotes so the shell-quoted -d body survives apostrophes in example data.
+        const json = JSON.stringify(body, null, 2).replace(/'/g, `'\\''`);
+        code += ` \\\n  -d '${json}'`;
       }
       if (responseExample) {
         code += `\n\n# Response: ${responseExample}`;
       }
       return code;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case SupportedLanguage.CURL: {
let url = `$FGA_API_URL${path}`;
if (pathParams) {
for (const [k, v] of Object.entries(pathParams)) {
url = url.split(`{${k}}`).join(v);
}
}
if (queryParams && Object.keys(queryParams).length > 0) {
const qs = Object.entries(queryParams)
.map(([k, v]) => `${k}=${v}`)
.join('&');
url += `?${qs}`;
}
let code = `curl -X ${method} '${url}' \\\n`;
code += ` -H 'Content-Type: application/json' \\\n`;
code += ` -H 'Authorization: Bearer $FGA_BEARER_TOKEN'`;
if (body) {
code += ` \\\n -d '${JSON.stringify(body, null, 2)}'`;
}
if (responseExample) {
code += `\n\n# Response: ${responseExample}`;
}
return code;
}
case SupportedLanguage.CURL: {
let url = `$FGA_API_URL${path}`;
if (pathParams) {
for (const [k, v] of Object.entries(pathParams)) {
url = url.split(`{${k}}`).join(encodeURIComponent(v));
}
}
if (queryParams && Object.keys(queryParams).length > 0) {
const qs = Object.entries(queryParams)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
url += `?${qs}`;
}
let code = `curl -X ${method} '${url}' \\\n`;
code += ` -H 'Content-Type: application/json' \\\n`;
code += ` -H 'Authorization: Bearer $FGA_BEARER_TOKEN'`;
if (body) {
// Escape single quotes so the shell-quoted -d body survives apostrophes in example data.
const json = JSON.stringify(body, null, 2).replace(/'/g, `'\\''`);
code += ` \\\n -d '${json}'`;
}
if (responseExample) {
code += `\n\n# Response: ${responseExample}`;
}
return code;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx` around lines
271 - 294, The curl branch in ExecuteApiRequestViewer.tsx builds URLs and bodies
without encoding or escaping: update the path param substitution (the url
mutation that does url.split(`{${k}}`).join(v)) to insert encodeURIComponent(v)
for each path value; when building the query string (the map producing
`${k}=${v}`) use encodeURIComponent for both key and value; and make the body
safe for shell by JSON.stringify(body, null, 2) and then escaping single quotes
inside that JSON (replace each ' with the shell-safe sequence '"'"') before
wrapping it in single quotes in the -d argument so the generated curl string
(the variable code) never breaks on apostrophes.


case SupportedLanguage.CLI:
case SupportedLanguage.CURL:
case SupportedLanguage.RPC:
case SupportedLanguage.PLAYGROUND:
return `# API Executor is only available through the SDKs`;
Expand All @@ -285,6 +330,7 @@ export function ExecuteApiRequestViewer(opts: ExecuteApiRequestViewerOpts): JSX.
SupportedLanguage.DOTNET_SDK,
SupportedLanguage.PYTHON_SDK,
SupportedLanguage.JAVA_SDK,
SupportedLanguage.CURL,
];
const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs);
return defaultOperationsViewer<ExecuteApiRequestViewerOpts>(allowedLanguages, opts, executeApiRequestViewer);
Expand All @@ -294,113 +340,160 @@ export function ExecuteApiRequestViewer(opts: ExecuteApiRequestViewerOpts): JSX.
// 2. ExecuteApiRequestStreamingViewer – configurable streaming request
// ---------------------------------------------------------------------------

interface ExecuteApiRequestStreamingViewerOpts extends ExecuteApiRequestViewerOpts {
/** Field name to extract from each streamed result, e.g. "object". */
responseField?: string;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface ExecuteApiRequestStreamingViewerOpts extends ExecuteApiRequestViewerOpts {}

function executeApiRequestStreamingViewer(lang: SupportedLanguage, opts: ExecuteApiRequestStreamingViewerOpts): string {
const { operationName, path, pathParams, body, responseField } = opts;
const field = responseField || 'object';
const { operationName, method, path, pathParams, body, queryParams } = opts;

switch (lang) {
case SupportedLanguage.JS_SDK: {
const parts: string[] = [];
parts.push(` operationName: "${operationName}",`);
parts.push(` method: "POST",`);
parts.push(` method: "${method}",`);
parts.push(` path: "${path}",`);
if (pathParams && Object.keys(pathParams).length > 0) {
const entries = Object.entries(pathParams)
.map(([k, v]) => `${k}: "${v}"`)
.join(', ');
parts.push(` pathParams: { ${entries} },`);
}
parts.push(` body: ${jsValue(body, 4)},`);
return `for await (const chunk of fgaClient.executeStreamedApiRequest({
if (body) {
parts.push(` body: ${jsValue(body, 4)},`);
}
if (queryParams && Object.keys(queryParams).length > 0) {
const entries = Object.entries(queryParams)
.map(([k, v]) => `${k}: "${v}"`)
.join(', ');
parts.push(` queryParams: { ${entries} },`);
}
return `import { parseNDJSONStream } from '@openfga/sdk';

const streamResp = await fgaClient.executeStreamedApiRequest({
${parts.join('\n')}
})) {
if (chunk.result) {
console.log(chunk.result.${field});
}
});

const source = streamResp.$response?.data ?? streamResp;

for await (const chunk of parseNDJSONStream(source)) {
console.log(chunk);
}`;
}

case SupportedLanguage.GO_SDK: {
let code = `requestBody := ${goValue(body, 8)}\n\n`;
let code = '';
if (body) {
code += `requestBody := ${goValue(body, 8)}\n\n`;
}
code += `request := openfga.NewAPIExecutorRequestBuilder(\n`;
code += ` "${operationName}", http.MethodPost, "${path}",\n)`;
code += ` "${operationName}", ${goMethod(method)}, "${path}",\n)`;
if (pathParams) {
for (const [k, v] of Object.entries(pathParams)) {
code += `.\n WithPathParameter("${k}", "${v}")`;
}
}
code += `.\n WithBody(requestBody).\n Build()\n\n`;
if (queryParams) {
for (const [k, v] of Object.entries(queryParams)) {
code += `.\n WithQueryParameter("${k}", "${v}")`;
}
}
if (body) {
code += `.\n WithBody(requestBody)`;
}
code += `.\n Build()\n\n`;
code += `channel, err := executor.ExecuteStreaming(ctx, request, openfga.DefaultStreamBufferSize)\n`;
code += `if err != nil {\n log.Fatalf("Streaming request failed: %v", err)\n}\ndefer channel.Close()\n\n`;
code += `for {\n select {\n case result, ok := <-channel.Results:\n if !ok {\n return\n }\n`;
code += ` var obj map[string]interface{}\n json.Unmarshal(result, &obj)\n`;
code += ` fmt.Println(obj["${field}"])\n`;
code += ` fmt.Println(string(result))\n`;
code += ` case err := <-channel.Errors:\n if err != nil {\n log.Fatalf("Stream error: %v", err)\n }\n }\n}`;
return code;
}

case SupportedLanguage.DOTNET_SDK: {
let code = `var request = RequestBuilder<object>\n`;
code += ` .Create(HttpMethod.Post, configuration.ApiUrl, "${path}")`;
code += ` .Create(${csharpMethod(method)}, configuration.ApiUrl, "${path}")`;
if (pathParams) {
for (const [k, v] of Object.entries(pathParams)) {
code += `\n .WithPathParameter("${k}", "${v}")`;
}
}
code += `\n .WithBody(${csharpValue(body, 8)});\n\n`;
code += `await foreach (var item in executor.ExecuteStreamingAsync<object, StreamedListObjectsResponse>(\n`;
if (queryParams) {
for (const [k, v] of Object.entries(queryParams)) {
code += `\n .WithQueryParameter("${k}", "${v}")`;
}
}
if (body) {
code += `\n .WithBody(${csharpValue(body, 8)})`;
}
code += `;\n\n`;
code += `await foreach (var item in executor.ExecuteStreamingAsync<object, Dictionary<string, object>>(\n`;
code += ` request, "${operationName}"))\n{\n`;
code += ` Console.WriteLine(item.${field.charAt(0).toUpperCase() + field.slice(1)});`;
code += ` Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(item));`;
code += `\n}`;
return code;
}

case SupportedLanguage.PYTHON_SDK: {
const parts: string[] = [];
parts.push(` operation_name="${operationName}",`);
parts.push(` method="POST",`);
parts.push(` method="${method}",`);
parts.push(` path="${path}",`);
if (pathParams && Object.keys(pathParams).length > 0) {
const entries = Object.entries(pathParams)
.map(([k, v]) => `"${k}": "${v}"`)
.join(', ');
parts.push(` path_params={${entries}},`);
}
parts.push(` body=${pyValue(body, 8)},`);
if (body) {
parts.push(` body=${pyValue(body, 8)},`);
}
if (queryParams && Object.keys(queryParams).length > 0) {
const entries = Object.entries(queryParams)
.map(([k, v]) => `"${k}": "${v}"`)
.join(', ');
parts.push(` query_params={${entries}},`);
}
return `async for chunk in fga_client.execute_streamed_api_request(
${parts.join('\n')}
):
if "result" in chunk:
print(chunk["result"]["${field}"])`;
print(chunk)`;
}

case SupportedLanguage.JAVA_SDK: {
let code = '';
code += `Map<String, Object> requestBody = ${javaValue(body, 8)};\n\n`;
if (body) {
code += `Map<String, Object> requestBody = ${javaValue(body, 8)};\n\n`;
}
code += `var request = ApiExecutorRequestBuilder.builder(\n`;
code += ` HttpMethod.POST, "${path}"\n)`;
code += ` ${javaMethod(method)}, "${path}"\n)`;
if (pathParams) {
Comment thread
rhamzeh marked this conversation as resolved.
for (const [k, v] of Object.entries(pathParams)) {
code += `\n .pathParam("${k}", "${v}")`;
}
}
code += `\n .body(requestBody)\n .build();\n\n`;
code += `fgaClient.streamingApiExecutor(StreamedListObjectsResponse.class)\n`;
if (queryParams) {
for (const [k, v] of Object.entries(queryParams)) {
code += `\n .queryParam("${k}", "${v}")`;
}
}
if (body) {
code += `\n .body(requestBody)`;
}
code += `\n .build();\n\n`;
code += `fgaClient.streamingApiExecutor(Map.class)\n`;
code += ` .stream(\n`;
code += ` request,\n`;
code += ` response -> System.out.println(response.get${field.charAt(0).toUpperCase() + field.slice(1)}()),\n`;
code += ` response -> System.out.println(response),\n`;
code += ` error -> System.err.println("Stream error: " + error.getMessage())\n`;
code += ` )\n .get();`;
return code;
}

case SupportedLanguage.CURL: {
return buildCurlSnippet({ method, path, pathParams, queryParams, body, streaming: true });
}
Comment on lines +492 to +494
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

curl streaming response no-buffer flag -N documentation

💡 Result:

The -N or --no-buffer flag in curl disables output buffering, enabling real-time streaming of responses as data arrives, rather than in buffered chunks. This is particularly useful for streaming responses like Server-Sent Events (SSE), live updates, or piping to other commands without delays. From the official curl manual: "-N, --no-buffer: Disables the buffering of the output stream. In normal work situations, curl will use a standard buffered output stream that will have the effect that it will output the data in chunks, not necessarily exactly when the data arrives. Using this option will disable that buffering. Note that this is the negated option name documented. You can thus use --buffer to enforce the buffering."[1][2][3][4] Example usage for streaming: curl -N https://example.com/stream-endpoint[5][6] This flag affects stdout output and is key for progressive display or processing of long-running responses.[7][8]

Citations:


🏁 Script executed:

# First, locate and read the file
fd -t f "ExecuteApiRequestViewer.tsx" | head -5

Repository: openfga/openfga.dev

Length of output: 125


🏁 Script executed:

# Get file size to determine how to read it
wc -l "src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx"

Repository: openfga/openfga.dev

Length of output: 129


🏁 Script executed:

# Read the relevant sections: lines 260-300 (non-streaming CURL), 315-330, and 460-490 (streaming CURL)
sed -n '260,300p; 315,330p; 460,490p' "src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx" -n

Repository: openfga/openfga.dev

Length of output: 3216


🏁 Script executed:

# Verify the full context of both functions to understand scope better
sed -n '245,310p; 455,500p' "src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx"

Repository: openfga/openfga.dev

Length of output: 3751


🏁 Script executed:

# Check for any other streaming-related CURL handling or configuration
rg -n "streaming|CURL|curl|no-buffer|-N" "src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx" | head -20

Repository: openfga/openfga.dev

Length of output: 609


Add -N flag and fix quote escaping in streaming CURL, plus handle missing responseExample comment.

The streaming CURL output has two issues:

  1. Missing -N / --no-buffer flag. Streaming endpoints require this flag to disable output buffering, otherwise users copying the command won't observe streaming behavior in real-time. Add -N immediately after curl.

  2. Inconsistent responseExample handling. The non-streaming CURL case (lines 271–304) includes if (responseExample) to emit a # Response: comment, but the streaming case omits this block entirely. This leaves the responseExample field silently unused in the streaming function's public API.

Additionally, both cases need quote escaping for the JSON body (use .replace(/'/g, "'\\''") when embedding in single-quoted shell strings), consistent with best practices for shell safety.

🛠️ Proposed fix
     case SupportedLanguage.CURL: {
       let url = `$FGA_API_URL${path}`;
       if (pathParams) {
         for (const [k, v] of Object.entries(pathParams)) {
           url = url.split(`{${k}}`).join(v);
         }
       }
       if (queryParams && Object.keys(queryParams).length > 0) {
         const qs = Object.entries(queryParams)
           .map(([k, v]) => `${k}=${v}`)
           .join('&');
         url += `?${qs}`;
       }
-      let code = `curl -X ${method} '${url}' \\\n`;
+      let code = `curl -N -X ${method} '${url}' \\\n`;
        code += `  -H 'Content-Type: application/json' \\\n`;
        code += `  -H 'Authorization: Bearer $FGA_BEARER_TOKEN'`;
        if (body) {
-        code += ` \\\n  -d '${JSON.stringify(body, null, 2)}'`;
+        const json = JSON.stringify(body, null, 2).replace(/'/g, `'\\''`);
+        code += ` \\\n  -d '${json}'`;
+      }
+      if (responseExample) {
+        code += `\n\n# Response: ${responseExample}`;
        }
        return code;
      }

Consider extracting the duplicated CURL builder logic (lines 271–304 and 465–490 are nearly identical) into a helper function that accepts a streaming: boolean parameter. This would eliminate ~20 lines of duplication and ensure encoding/streaming fixes are applied consistently in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx` around lines
465 - 485, In the SupportedLanguage.CURL branch inside
ExecuteApiRequestViewer.tsx (the CURL case building the `code` string for
method/path/url/body/responseExample), add the streaming-specific fixes: insert
`-N` (or `--no-buffer`) immediately after `curl` when building the command for
streaming endpoints, ensure any JSON body embedded in single quotes is
shell-safe by escaping single quotes (use .replace(/'/g, "'\\''") on
JSON.stringify(body, null, 2)), and emit the same `# Response:` comment using
`responseExample` as the non-streaming case so the field is not ignored;
optionally factor the duplicated CURL construction into a helper like
`buildCurlCommand({method, url, body, responseExample, streaming})` and call it
from both the non-streaming and streaming branches to keep behavior consistent.


case SupportedLanguage.CLI:
case SupportedLanguage.CURL:
case SupportedLanguage.RPC:
case SupportedLanguage.PLAYGROUND:
return `# API Executor streaming is only available through the SDKs`;
Expand All @@ -416,6 +509,7 @@ export function ExecuteApiRequestStreamingViewer(opts: ExecuteApiRequestStreamin
SupportedLanguage.DOTNET_SDK,
SupportedLanguage.PYTHON_SDK,
SupportedLanguage.JAVA_SDK,
SupportedLanguage.CURL,
];
const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs);
return defaultOperationsViewer<ExecuteApiRequestStreamingViewerOpts>(
Expand Down
Loading