Skip to content

Commit 5bea0bb

Browse files
authored
axios hook fixes (#11)
1 parent 1c07104 commit 5bea0bb

File tree

3 files changed

+207
-108
lines changed

3 files changed

+207
-108
lines changed

src/web/assets/axioshook/dist-dev/index-dev.js

Lines changed: 79 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@
3131
csrfTokenValue: tokenValue ?? ""
3232
};
3333
};
34+
const replaceEmptyArrays = (obj, maxDepth = 10, currentDepth = 0) => {
35+
if (currentDepth > maxDepth) {
36+
return obj;
37+
}
38+
if (Array.isArray(obj)) {
39+
return obj.map(
40+
(item) => replaceEmptyArrays(item, maxDepth, currentDepth + 1)
41+
);
42+
} else if (typeof obj === "object" && obj !== null) {
43+
return Object.fromEntries(
44+
Object.entries(obj).map(([key, value]) => [
45+
key,
46+
Array.isArray(value) && value.length === 0 ? "" : replaceEmptyArrays(value, maxDepth, currentDepth + 1)
47+
])
48+
);
49+
}
50+
return obj;
51+
};
52+
const getContentType = (headers) => {
53+
if (typeof headers.get === "function") {
54+
return headers.get("content-type");
55+
}
56+
for (const key in headers) {
57+
if (key.toLowerCase() === "content-type") {
58+
return headers[key];
59+
}
60+
}
61+
return void 0;
62+
};
3463
const setCsrfOnMeta = (csrfTokenName, csrfTokenValue) => {
3564
let csrfMetaEl = document.head.querySelector("meta[csrf]");
3665
if (csrfMetaEl) {
@@ -45,60 +74,65 @@
4574
}
4675
};
4776
const configureAxios = async () => {
48-
window.axios.defaults.headers = {
49-
"Content-Type": "multipart/form-data"
50-
};
5177
window.axios.interceptors.request.use(async (config) => {
52-
if (config.method === "post" || config.method === "put") {
53-
let csrfMeta = getTokenFromMeta();
54-
if (!csrfMeta) {
55-
sessionInfo = await getSessionInfo();
56-
if (!sessionInfo.isGuest) {
57-
setCsrfOnMeta(sessionInfo.csrfTokenName, sessionInfo.csrfTokenValue);
58-
csrfMeta = getTokenFromMeta();
59-
}
60-
}
61-
const csrf = csrfMeta || sessionInfo;
62-
if (!csrf) {
63-
throw new Error("CSRF token not found");
78+
if (config.method !== "post" && config.method !== "put") {
79+
return config;
80+
}
81+
let csrfMeta = getTokenFromMeta();
82+
if (!csrfMeta) {
83+
sessionInfo = await getSessionInfo();
84+
if (!sessionInfo.isGuest) {
85+
setCsrfOnMeta(sessionInfo.csrfTokenName, sessionInfo.csrfTokenValue);
86+
csrfMeta = getTokenFromMeta();
6487
}
65-
const actionPath = getActionPath(config.url);
66-
config.url = "";
67-
if (config.data instanceof FormData) {
68-
config.data.append(csrf.csrfTokenName, csrf.csrfTokenValue);
88+
}
89+
const csrf = csrfMeta || sessionInfo;
90+
if (!csrf) {
91+
throw new Error(
92+
"Inertia (Craft): CSRF token not found. Ensure session is initialized or meta tag is present."
93+
);
94+
}
95+
const actionPath = getActionPath(config.url ?? "");
96+
if (getContentType(config.headers) == void 0) {
97+
config.headers.set("Content-Type", "application/x-www-form-urlencoded");
98+
}
99+
if (config.data instanceof FormData) {
100+
if (!config.data.has("action")) {
69101
config.data.append("action", actionPath);
70-
} else {
71-
const replaceEmptyArrays = (obj) => {
72-
if (Array.isArray(obj)) {
73-
return obj.map((item) => replaceEmptyArrays(item));
74-
} else if (typeof obj === "object" && obj !== null) {
75-
return Object.fromEntries(
76-
Object.entries(obj).map(([key, value]) => [
77-
key,
78-
Array.isArray(value) && value.length === 0 ? "" : replaceEmptyArrays(value)
79-
])
80-
);
81-
}
82-
return obj;
83-
};
84-
const contentType = config.headers["Content-Type"] || config.headers["content-type"] || config.headers["CONTENT-TYPE"];
85-
let data = {
86-
[csrf.csrfTokenName]: csrf.csrfTokenValue,
87-
action: actionPath,
88-
...config.data
89-
};
90-
if (typeof contentType === "string" && contentType.toLowerCase().includes("multipart/form-data")) {
91-
data = replaceEmptyArrays(data);
92-
}
93-
config.data = data;
102+
config.url = "";
103+
}
104+
config.data.append(csrf.csrfTokenName, csrf.csrfTokenValue);
105+
} else {
106+
let data = {
107+
[csrf.csrfTokenName]: csrf.csrfTokenValue,
108+
action: actionPath,
109+
...config.data
110+
};
111+
const contentType = getContentType(config.headers ?? {});
112+
if (typeof contentType === "string" && contentType.toLowerCase().includes("multipart/form-data")) {
113+
data = replaceEmptyArrays(data);
94114
}
115+
config.data = data;
95116
}
96117
return config;
97118
});
98119
window.axios.interceptors.response.use(
99120
async (response) => {
100-
var _a;
101-
if (((_a = response.config.data) == null ? void 0 : _a.get("action")) == "users/login") {
121+
let action = null;
122+
if (response.config.data instanceof FormData) {
123+
action = response.config.data.get("action");
124+
} else if (typeof response.config.data === "object" && response.config.data !== null) {
125+
action = response.config.data.action;
126+
} else if (typeof response.config.data === "string") {
127+
try {
128+
const parsed = JSON.parse(response.config.data);
129+
action = parsed.action;
130+
} catch {
131+
const params = new URLSearchParams(response.config.data);
132+
action = params.get("action");
133+
}
134+
}
135+
if (action === "users/login") {
102136
await getSessionInfo().then((sessionInfo2) => {
103137
setCsrfOnMeta(sessionInfo2.csrfTokenName, sessionInfo2.csrfTokenValue);
104138
});

src/web/assets/axioshook/dist/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/web/assets/axioshook/src/index.ts

Lines changed: 127 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AxiosInstance } from "axios";
1+
import type { AxiosInstance, AxiosHeaders } from "axios";
22

33
declare global {
44
interface Window {
@@ -33,6 +33,22 @@ const getSessionInfo = async function (): Promise<SessionInfo> {
3333
// Don't store the promise, store the session info once it's resolved
3434
let sessionInfo: SessionInfo | null = null;
3535

36+
/**
37+
* Craft CMS submission requirements:
38+
*
39+
* - If specifying application/json as content-type header:
40+
* - Using Inertia's Form component, include the "action" parameter
41+
* - <Form method="post" action="/actions/...">
42+
* - Or POST directly to a /actions/ endpoint.
43+
* - (useForm) form.post("/actions/...")
44+
*
45+
* - Default: If using application/x-www-form-urlencoded content-type header:
46+
* - Include "action" parameter in the form data (no /actions/ prefix)
47+
* - <input type="hidden" name="action" value="entries/save-entry">
48+
* - Or POST to the current URL ("")
49+
* - (useForm) form.post("")
50+
*/
51+
3652
const getActionPath = (url: string) => {
3753
const postPathObject: URL = new URL(url);
3854
const postPathPathname: string = postPathObject.pathname;
@@ -63,6 +79,48 @@ const getTokenFromMeta = (): csrfMeta | null => {
6379
};
6480
};
6581

82+
/**
83+
* Replaces empty arrays in an object with an empty string, up to a max depth.
84+
* @param obj The object to process
85+
* @param maxDepth Maximum depth to traverse (default: 10)
86+
* @param currentDepth Current depth (for internal use)
87+
*/
88+
const replaceEmptyArrays = (obj: any, maxDepth = 10, currentDepth = 0): any => {
89+
if (currentDepth > maxDepth) {
90+
return obj;
91+
}
92+
if (Array.isArray(obj)) {
93+
return obj.map((item) =>
94+
replaceEmptyArrays(item, maxDepth, currentDepth + 1)
95+
);
96+
} else if (typeof obj === "object" && obj !== null) {
97+
return Object.fromEntries(
98+
Object.entries(obj).map(([key, value]) => [
99+
key,
100+
Array.isArray(value) && value.length === 0
101+
? ""
102+
: replaceEmptyArrays(value, maxDepth, currentDepth + 1),
103+
])
104+
);
105+
}
106+
return obj;
107+
};
108+
109+
const getContentType = (
110+
headers: AxiosHeaders | Record<string, any>
111+
): string | undefined => {
112+
// AxiosHeaders may have a .get() method, otherwise treat as plain object
113+
if (typeof (headers as any).get === "function") {
114+
return (headers as any).get("content-type");
115+
}
116+
for (const key in headers) {
117+
if (key.toLowerCase() === "content-type") {
118+
return headers[key];
119+
}
120+
}
121+
return undefined;
122+
};
123+
66124
const setCsrfOnMeta = (csrfTokenName: string, csrfTokenValue: string): void => {
67125
// Check if a CSRF meta element already exists
68126
let csrfMetaEl = document.head.querySelector("meta[csrf]");
@@ -82,83 +140,90 @@ const setCsrfOnMeta = (csrfTokenName: string, csrfTokenValue: string): void => {
82140
};
83141

84142
const configureAxios = async () => {
85-
window.axios.defaults.headers = {
86-
"Content-Type": "multipart/form-data",
87-
};
88-
89143
(window.axios as AxiosInstance).interceptors.request.use(async (config) => {
90-
if (config.method === "post" || config.method === "put") {
91-
let csrfMeta = getTokenFromMeta();
92-
if (!csrfMeta) {
93-
// Wait for the session info to be resolved before configuring axios
94-
sessionInfo = await getSessionInfo();
95-
if (!sessionInfo.isGuest) {
96-
setCsrfOnMeta(sessionInfo.csrfTokenName, sessionInfo.csrfTokenValue);
97-
csrfMeta = getTokenFromMeta();
98-
}
144+
if (config.method !== "post" && config.method !== "put") {
145+
return config;
146+
}
147+
148+
let csrfMeta = getTokenFromMeta();
149+
if (!csrfMeta) {
150+
// Wait for the session info to be resolved before configuring axios
151+
sessionInfo = await getSessionInfo();
152+
if (!sessionInfo.isGuest) {
153+
setCsrfOnMeta(sessionInfo.csrfTokenName, sessionInfo.csrfTokenValue);
154+
csrfMeta = getTokenFromMeta();
99155
}
156+
}
100157

101-
const csrf = csrfMeta || sessionInfo;
158+
const csrf = csrfMeta || sessionInfo;
102159

103-
if (!csrf) {
104-
throw new Error("CSRF token not found");
105-
}
160+
if (!csrf) {
161+
throw new Error(
162+
"Inertia (Craft): CSRF token not found. Ensure session is initialized or meta tag is present."
163+
);
164+
}
106165

107-
const actionPath = getActionPath(config.url);
166+
const actionPath = getActionPath(config.url ?? "");
108167

109-
config.url = "";
168+
if (getContentType(config.headers) == undefined) {
169+
config.headers.set("Content-Type", "application/x-www-form-urlencoded");
170+
}
110171

111-
if (config.data instanceof FormData) {
112-
config.data.append(csrf.csrfTokenName, csrf.csrfTokenValue);
172+
if (config.data instanceof FormData) {
173+
if (!config.data.has("action")) {
113174
config.data.append("action", actionPath);
114-
// NOTE: FormData cannot represent empty arrays. If you need to send empty arrays,
115-
// add a placeholder value (e.g., an empty string or special marker) when building the FormData.
116-
// Example:
117-
// if (myArray.length === 0) formData.append('myArray', '');
118-
} else {
119-
// For plain objects, replace empty arrays with empty strings before sending
120-
const replaceEmptyArrays = (obj: any): any => {
121-
if (Array.isArray(obj)) {
122-
return obj.map((item) => replaceEmptyArrays(item));
123-
} else if (typeof obj === "object" && obj !== null) {
124-
return Object.fromEntries(
125-
Object.entries(obj).map(([key, value]) => [
126-
key,
127-
Array.isArray(value) && value.length === 0
128-
? ""
129-
: replaceEmptyArrays(value),
130-
])
131-
);
132-
}
133-
return obj;
134-
};
135-
136-
const contentType =
137-
config.headers["Content-Type"] ||
138-
config.headers["content-type"] ||
139-
config.headers["CONTENT-TYPE"];
140-
141-
let data = {
142-
[csrf.csrfTokenName]: csrf.csrfTokenValue,
143-
action: actionPath,
144-
...config.data,
145-
};
146-
if (
147-
typeof contentType === "string" &&
148-
contentType.toLowerCase().includes("multipart/form-data")
149-
) {
150-
data = replaceEmptyArrays(data);
151-
}
152-
config.data = data;
175+
config.url = "";
176+
}
177+
config.data.append(csrf.csrfTokenName, csrf.csrfTokenValue);
178+
179+
/** NOTE: FormData cannot represent empty arrays. If you need to send empty arrays as values,
180+
* add a placeholder value (e.g., an empty string or special marker) when building the FormData.
181+
* eg, if (myArray.length === 0) formData.append('myArray', '');
182+
*/
183+
} else {
184+
let data = {
185+
[csrf.csrfTokenName]: csrf.csrfTokenValue,
186+
action: actionPath,
187+
...config.data,
188+
};
189+
190+
const contentType = getContentType(config.headers ?? {});
191+
if (
192+
typeof contentType === "string" &&
193+
contentType.toLowerCase().includes("multipart/form-data")
194+
) {
195+
data = replaceEmptyArrays(data);
153196
}
197+
config.data = data;
154198
}
199+
155200
return config;
156201
});
157202

158203
// Add a response interceptor
159204
(window.axios as AxiosInstance).interceptors.response.use(
160205
async (response) => {
161-
if (response.config.data?.get("action") == "users/login") {
206+
// Support both FormData and plain object/stringified data
207+
let action = null;
208+
if (response.config.data instanceof FormData) {
209+
action = response.config.data.get("action");
210+
} else if (
211+
typeof response.config.data === "object" &&
212+
response.config.data !== null
213+
) {
214+
action = response.config.data.action;
215+
} else if (typeof response.config.data === "string") {
216+
// Try to parse as JSON or URL-encoded
217+
try {
218+
const parsed = JSON.parse(response.config.data);
219+
action = parsed.action;
220+
} catch {
221+
// Try URLSearchParams
222+
const params = new URLSearchParams(response.config.data);
223+
action = params.get("action");
224+
}
225+
}
226+
if (action === "users/login") {
162227
await getSessionInfo().then((sessionInfo) => {
163228
setCsrfOnMeta(sessionInfo.csrfTokenName, sessionInfo.csrfTokenValue);
164229
});

0 commit comments

Comments
 (0)