Skip to content

Commit 4e5d829

Browse files
authored
fix: handle headers in graphql resource (#5395)
1 parent e87cd94 commit 4e5d829

File tree

2 files changed

+63
-50
lines changed

2 files changed

+63
-50
lines changed

apps/builder/app/builder/features/settings-panel/resource-panel.tsx

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,17 @@ import {
6666
type InstancePath,
6767
} from "~/shared/awareness";
6868
import { updateWebstudioData } from "~/shared/instance-utils";
69-
import {
70-
computeExpression,
71-
rebindTreeVariablesMutable,
72-
} from "~/shared/data-variables";
69+
import { rebindTreeVariablesMutable } from "~/shared/data-variables";
7370
import { parseCurl, type CurlRequest } from "./curl";
7471

7572
export const parseResource = ({
7673
id,
74+
control,
7775
name,
7876
formData,
7977
}: {
8078
id: string;
79+
control?: string;
8180
name?: string;
8281
formData: FormData;
8382
}) => {
@@ -87,6 +86,7 @@ export const parseResource = ({
8786
const headerValues = formData.getAll("header-value") as string[];
8887
return Resource.parse({
8988
id,
89+
control,
9090
name: name ?? formData.get("name"),
9191
url: formData.get("url"),
9292
searchParams: searchParamNames
@@ -253,7 +253,7 @@ const SearchParamPair = ({
253253
}) => {
254254
const evaluatedValue = evaluateExpressionWithinScope(value, scope);
255255
// expressions with variables or objects cannot be edited from input
256-
const isValueUnbound =
256+
const isValueUnboundString =
257257
isLiteralExpression(value) && typeof evaluatedValue === "string";
258258
return (
259259
<Grid
@@ -274,7 +274,7 @@ const SearchParamPair = ({
274274
<InputField
275275
placeholder="Value"
276276
name="search-param-value-literal"
277-
disabled={!isValueUnbound}
277+
disabled={!isValueUnboundString}
278278
value={serializeValue(evaluatedValue)}
279279
// update text value as string literal
280280
onChange={(event) =>
@@ -284,7 +284,7 @@ const SearchParamPair = ({
284284
<BindingPopover
285285
scope={scope}
286286
aliases={aliases}
287-
variant={isValueUnbound ? "default" : "bound"}
287+
variant={isLiteralExpression(value) ? "default" : "bound"}
288288
value={value}
289289
onChange={(newValue) => onChange(name, newValue)}
290290
onRemove={(evaluatedValue) =>
@@ -377,7 +377,7 @@ const HeaderPair = ({
377377
}) => {
378378
const evaluatedValue = evaluateExpressionWithinScope(value, scope);
379379
// expressions with variables or objects cannot be edited from input
380-
const isValueUnbound =
380+
const isValueUnboundString =
381381
isLiteralExpression(value) && typeof evaluatedValue === "string";
382382
return (
383383
<Grid
@@ -398,7 +398,7 @@ const HeaderPair = ({
398398
<InputField
399399
placeholder="Value"
400400
name="header-value-validator"
401-
disabled={!isValueUnbound}
401+
disabled={!isValueUnboundString}
402402
value={serializeValue(evaluatedValue)}
403403
// update text value as string literal
404404
onChange={(event) =>
@@ -408,7 +408,7 @@ const HeaderPair = ({
408408
<BindingPopover
409409
scope={scope}
410410
aliases={aliases}
411-
variant={isValueUnbound ? "default" : "bound"}
411+
variant={isLiteralExpression(value) ? "default" : "bound"}
412412
value={value}
413413
onChange={(newValue) => onChange(name, newValue)}
414414
onRemove={(evaluatedValue) =>
@@ -558,7 +558,7 @@ export const getResourceScopeForInstance = ({
558558
const name = encodeDataVariableId(dataSourceId);
559559
scope[name] = value;
560560
aliases.set(name, dataSource.name);
561-
variableValues.set(dataSource.name, value);
561+
variableValues.set(dataSourceId, value);
562562
}
563563
}
564564
}
@@ -621,7 +621,7 @@ export const useResourceScope = ({ variable }: { variable?: DataSource }) => {
621621
const key = encodeDataVariableId(variable.id);
622622
delete newScope[key];
623623
newAliases.delete(key);
624-
newVariableValues.delete(variable.name);
624+
newVariableValues.delete(variable.id);
625625
}
626626
return {
627627
scope: newScope,
@@ -796,7 +796,10 @@ const parseHeaders = (headers: Resource["headers"]) => {
796796
let maxAge: undefined | string;
797797
let bodyType: BodyType;
798798
const newHeaders = headers.filter((header) => {
799-
const value = computeExpression(header.value, new Map()).toLowerCase();
799+
// cast raw expression result to string
800+
const value = String(
801+
evaluateExpressionWithinScope(header.value, {})
802+
).toLowerCase();
800803
if (isCacheControl(header.name)) {
801804
// move simple header like Cache-Control: max-age=10 to dedicated input
802805
// preserve more complex cache-control
@@ -818,7 +821,7 @@ const parseHeaders = (headers: Resource["headers"]) => {
818821
return false;
819822
}
820823
}
821-
return false;
824+
return true;
822825
});
823826
return { headers: newHeaders, maxAge, bodyType };
824827
};
@@ -980,8 +983,6 @@ export const SystemResourceForm = forwardRef<
980983
? resources.get(variable.resourceId)
981984
: undefined;
982985

983-
const method = "get";
984-
985986
const localResources = [
986987
{
987988
label: "Sitemap",
@@ -1006,19 +1007,15 @@ export const SystemResourceForm = forwardRef<
10061007
if (scopeInstanceId === undefined) {
10071008
return;
10081009
}
1009-
const name = z.string().parse(formData.get("name"));
1010-
const newResource: Resource = {
1010+
const newResource: Resource = parseResource({
10111011
id: resource?.id ?? nanoid(),
1012-
name,
10131012
control: "system",
1014-
url: localResource.value,
1015-
method,
1016-
headers: [],
1017-
};
1013+
formData,
1014+
});
10181015
const newVariable: DataSource = {
10191016
id: variable?.id ?? nanoid(),
10201017
scopeInstanceId,
1021-
name,
1018+
name: newResource.name,
10221019
type: "resource",
10231020
resourceId: newResource.id,
10241021
};
@@ -1037,6 +1034,8 @@ export const SystemResourceForm = forwardRef<
10371034

10381035
return (
10391036
<>
1037+
<input type="hidden" name="method" value="get" />
1038+
<input type="hidden" name="url" value={localResource.value} />
10401039
<Flex direction="column" css={{ gap: theme.spacing[3] }}>
10411040
<Label htmlFor={resourceId}>Resource</Label>
10421041
<Select
@@ -1116,29 +1115,15 @@ export const GraphqlResourceForm = forwardRef<
11161115
if (scopeInstanceId === undefined) {
11171116
return;
11181117
}
1119-
const name = z.string().parse(formData.get("name"));
1120-
const body = generateObjectExpression(
1121-
new Map([
1122-
["query", JSON.stringify(query)],
1123-
["variables", variables],
1124-
])
1125-
);
1126-
const newResource: Resource = {
1118+
const newResource = parseResource({
11271119
id: resource?.id ?? nanoid(),
1128-
name,
11291120
control: "graphql",
1130-
url,
1131-
method: "post",
1132-
headers: [
1133-
...headers,
1134-
{ name: "Content-Type", value: "application/json" },
1135-
],
1136-
body,
1137-
};
1121+
formData,
1122+
});
11381123
const newVariable: DataSource = {
11391124
id: variable?.id ?? nanoid(),
11401125
scopeInstanceId,
1141-
name,
1126+
name: newResource.name,
11421127
type: "resource",
11431128
resourceId: newResource.id,
11441129
};
@@ -1155,6 +1140,28 @@ export const GraphqlResourceForm = forwardRef<
11551140

11561141
return (
11571142
<>
1143+
<input type="hidden" name="method" value="post" />
1144+
{!headers.some(({ name }) => isContentType(name)) && (
1145+
<>
1146+
<input type="hidden" name="header-name" value="Content-Type" />
1147+
<input
1148+
type="hidden"
1149+
name="header-value"
1150+
value={`"application/json"`}
1151+
/>
1152+
</>
1153+
)}
1154+
<input
1155+
type="hidden"
1156+
name="body"
1157+
value={generateObjectExpression(
1158+
new Map([
1159+
["query", JSON.stringify(query)],
1160+
["variables", variables],
1161+
])
1162+
)}
1163+
/>
1164+
11581165
<UrlField
11591166
scope={scope}
11601167
aliases={aliases}

packages/sdk/src/resource-loader.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,19 @@ export const loadResource = async (
2525
) => {
2626
try {
2727
const { method, searchParams, headers, body } = resourceRequest;
28-
// cloudflare workers fail when fetching url contains spaces
29-
// even though new URL suppose to trim them on parsing by spec
30-
const url = new URL(resourceRequest.url.trim());
31-
if (searchParams) {
32-
for (const { name, value } of searchParams) {
33-
url.searchParams.append(name, serializeValue(value));
28+
let href = resourceRequest.url;
29+
try {
30+
// cloudflare workers fail when fetching url contains spaces
31+
// even though new URL suppose to trim them on parsing by spec
32+
const url = new URL(resourceRequest.url.trim());
33+
if (searchParams) {
34+
for (const { name, value } of searchParams) {
35+
url.searchParams.append(name, serializeValue(value));
36+
}
3437
}
38+
href = url.href;
39+
} catch {
40+
// empty block
3541
}
3642
const requestHeaders = new Headers(
3743
headers.map(({ name, value }): [string, string] => [
@@ -46,7 +52,7 @@ export const loadResource = async (
4652
if (method !== "get" && body !== undefined) {
4753
requestInit.body = serializeValue(body);
4854
}
49-
const response = await customFetch(url.href, requestInit);
55+
const response = await customFetch(href, requestInit);
5056

5157
let data = await response.text();
5258

@@ -59,7 +65,7 @@ export const loadResource = async (
5965

6066
if (!response.ok) {
6167
console.error(
62-
`Failed to load resource: ${url} - ${response.status}: ${JSON.stringify(data).slice(0, 300)}`
68+
`Failed to load resource: ${href} - ${response.status}: ${JSON.stringify(data).slice(0, 300)}`
6369
);
6470
}
6571

0 commit comments

Comments
 (0)