Skip to content

Commit 5943a8a

Browse files
alishaz-polymathdevin-ai-integration[bot]bot_apk
authored
fix: strip avatar and profile from children payload in managed event type updates (#28239)
* fix: strip avatar and profile from children payload in managed event type updates When assigning users to managed event types with ~85+ users, the request body exceeds the 1MB server limit. The root cause is that the full ChildrenEventType objects (including avatar, profile, username, membership) are sent in the update payload, even though the server schema (childSchema) only needs owner.{id, name, email, eventTypeSlugs} and hidden. Avatar data can be particularly large when stored as base64 data URLs. With 85 users each having ~10KB+ avatars, the payload easily exceeds 1MB. This fix strips the children array down to only server-required fields before sending the mutation, while keeping the full data in form state for UI display purposes. Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * refactor: extract stripChildrenForPayload into shared utility instead of duplicating in test Addresses Cubic AI review feedback: the test file was duplicating the stripping logic instead of importing it from production code. - Extracted stripChildrenForPayload() into childrenEventType.ts - Updated useEventTypeForm.ts to import and use the shared function - Updated test file to import from production code instead of defining its own copy Co-Authored-By: bot_apk <apk@cognition.ai> * fix: remove unused @ts-expect-error directive in useEventTypeForm Co-Authored-By: bot_apk <apk@cognition.ai> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: bot_apk <apk@cognition.ai>
1 parent ec7f8dd commit 5943a8a

File tree

3 files changed

+185
-12
lines changed

3 files changed

+185
-12
lines changed

packages/features/eventtypes/lib/childrenEventType.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,21 @@ export type ChildrenEventType = {
1818
slug: string;
1919
hidden: boolean;
2020
};
21+
22+
/**
23+
* Extracts only the fields needed by the server from a ChildrenEventType array.
24+
* Display-only fields (avatar, profile, username, membership) are stripped to
25+
* avoid bloating the request payload — with many assigned users (~85+),
26+
* sending full objects can push the request body over the 1MB server limit.
27+
*/
28+
export function stripChildrenForPayload(children: ChildrenEventType[]) {
29+
return children.map((child) => ({
30+
hidden: child.hidden,
31+
owner: {
32+
id: child.owner.id,
33+
name: child.owner.name,
34+
email: child.owner.email,
35+
eventTypeSlugs: child.owner.eventTypeSlugs,
36+
},
37+
}));
38+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { describe, it, expect } from "vitest";
2+
3+
import type { ChildrenEventType } from "@calcom/features/eventtypes/lib/childrenEventType";
4+
import { stripChildrenForPayload } from "@calcom/features/eventtypes/lib/childrenEventType";
5+
import { MembershipRole } from "@calcom/prisma/enums";
6+
7+
describe("useEventTypeForm - children payload stripping", () => {
8+
it("should strip avatar, profile, username, and membership from children payload", () => {
9+
const children: ChildrenEventType[] = [
10+
{
11+
value: "1",
12+
label: "Alice",
13+
created: true,
14+
slug: "test-event",
15+
hidden: false,
16+
owner: {
17+
id: 1,
18+
name: "Alice",
19+
email: "alice@example.com",
20+
username: "alice",
21+
avatar: "data:image/png;base64," + "A".repeat(50000), // Large base64 avatar
22+
membership: MembershipRole.MEMBER,
23+
eventTypeSlugs: ["meeting", "consultation"],
24+
profile: {
25+
id: 1,
26+
username: "alice",
27+
upId: "usr_1",
28+
organizationId: null,
29+
organization: null,
30+
},
31+
},
32+
},
33+
{
34+
value: "2",
35+
label: "Bob",
36+
created: false,
37+
slug: "test-event",
38+
hidden: true,
39+
owner: {
40+
id: 2,
41+
name: "Bob",
42+
email: "bob@example.com",
43+
username: "bob",
44+
avatar: "https://example.com/avatars/bob.png",
45+
membership: MembershipRole.OWNER,
46+
eventTypeSlugs: [],
47+
profile: {
48+
id: 2,
49+
username: "bob",
50+
upId: "usr_2",
51+
organizationId: 10,
52+
organization: {
53+
id: 10,
54+
slug: "org",
55+
name: "Org",
56+
calVideoLogo: null,
57+
bannerUrl: "",
58+
isPlatform: false,
59+
},
60+
},
61+
},
62+
},
63+
];
64+
65+
const stripped = stripChildrenForPayload(children);
66+
67+
// Should only contain server-needed fields
68+
expect(stripped).toEqual([
69+
{
70+
hidden: false,
71+
owner: {
72+
id: 1,
73+
name: "Alice",
74+
email: "alice@example.com",
75+
eventTypeSlugs: ["meeting", "consultation"],
76+
},
77+
},
78+
{
79+
hidden: true,
80+
owner: {
81+
id: 2,
82+
name: "Bob",
83+
email: "bob@example.com",
84+
eventTypeSlugs: [],
85+
},
86+
},
87+
]);
88+
89+
// Verify avatar is not present
90+
for (const child of stripped) {
91+
expect(child.owner).not.toHaveProperty("avatar");
92+
expect(child.owner).not.toHaveProperty("profile");
93+
expect(child.owner).not.toHaveProperty("username");
94+
expect(child.owner).not.toHaveProperty("membership");
95+
}
96+
});
97+
98+
it("should significantly reduce payload size for large teams", () => {
99+
// Simulate 85 users with base64 avatars (~10KB each)
100+
const largeBase64Avatar = "data:image/png;base64," + "A".repeat(10000);
101+
102+
const children: ChildrenEventType[] = Array.from({ length: 85 }, (_, i) => ({
103+
value: String(i + 1),
104+
label: `User ${i + 1}`,
105+
created: true,
106+
slug: "managed-event",
107+
hidden: false,
108+
owner: {
109+
id: i + 1,
110+
name: `User ${i + 1}`,
111+
email: `user${i + 1}@example.com`,
112+
username: `user${i + 1}`,
113+
avatar: largeBase64Avatar,
114+
membership: MembershipRole.MEMBER,
115+
eventTypeSlugs: ["event-a", "event-b"],
116+
profile: {
117+
id: i + 1,
118+
username: `user${i + 1}`,
119+
upId: `usr_${i + 1}`,
120+
organizationId: 1,
121+
organization: {
122+
id: 1,
123+
slug: "org",
124+
name: "Large Org",
125+
calVideoLogo: null,
126+
bannerUrl: "",
127+
isPlatform: false,
128+
},
129+
},
130+
},
131+
}));
132+
133+
const fullPayloadSize = JSON.stringify(children).length;
134+
const strippedPayloadSize = JSON.stringify(stripChildrenForPayload(children)).length;
135+
136+
// The stripped payload should be dramatically smaller
137+
expect(strippedPayloadSize).toBeLessThan(fullPayloadSize * 0.1);
138+
139+
// Full payload with 85 users and 10KB avatars should be around 850KB+
140+
expect(fullPayloadSize).toBeGreaterThan(800000);
141+
142+
// Stripped payload should be well under 1MB
143+
expect(strippedPayloadSize).toBeLessThan(100000);
144+
});
145+
146+
it("should handle undefined children gracefully", () => {
147+
const children: ChildrenEventType[] = [];
148+
const stripped = stripChildrenForPayload(children);
149+
expect(stripped).toEqual([]);
150+
});
151+
});

packages/platform/atoms/event-types/hooks/useEventTypeForm.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1-
import { zodResolver } from "@hookform/resolvers/zod";
2-
import { useMemo, useState, useEffect } from "react";
3-
import { useForm } from "react-hook-form";
4-
import { z } from "zod";
5-
61
import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps";
72
import { locationsResolver } from "@calcom/app-store/locations";
8-
import { DEFAULT_PROMPT_VALUE, DEFAULT_BEGIN_MESSAGE } from "@calcom/features/calAIPhone/promptTemplates";
3+
import { DEFAULT_BEGIN_MESSAGE, DEFAULT_PROMPT_VALUE } from "@calcom/features/calAIPhone/promptTemplates";
94
import type { TemplateType } from "@calcom/features/calAIPhone/zod-utils";
105
import { validateCustomEventName } from "@calcom/features/eventtypes/lib/eventNaming";
11-
import { sortHosts } from "@calcom/lib/bookings/hostGroupUtils";
6+
import { stripChildrenForPayload } from "@calcom/features/eventtypes/lib/childrenEventType";
127
import type {
13-
FormValues,
148
EventTypeSetupProps,
159
EventTypeUpdateInput,
10+
FormValues,
1611
} from "@calcom/features/eventtypes/lib/types";
12+
import { sortHosts } from "@calcom/lib/bookings/hostGroupUtils";
1713
import { useLocale } from "@calcom/lib/hooks/useLocale";
1814
import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder";
1915
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
2016
import { eventTypeBookingFields as eventTypeBookingFieldsSchema } from "@calcom/prisma/zod-utils";
17+
import { zodResolver } from "@hookform/resolvers/zod";
18+
import { useEffect, useMemo, useState } from "react";
19+
import { useForm } from "react-hook-form";
20+
import { z } from "zod";
2121

2222
type Fields = z.infer<typeof eventTypeBookingFieldsSchema>;
2323

@@ -287,13 +287,11 @@ export const useEventTypeForm = ({
287287
const updatedFields: Partial<FormValues> = {};
288288
Object.keys(dirtyFields).forEach((key) => {
289289
const typedKey = key as keyof typeof dirtyFields;
290-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
291-
// @ts-ignore
292290
updatedFields[typedKey] = undefined;
293291
const isDirty = isFieldDirty(typedKey);
294292
if (isDirty) {
295293
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
296-
// @ts-ignore
294+
// @ts-expect-error
297295
updatedFields[typedKey] = values[typedKey];
298296
}
299297
});
@@ -381,6 +379,12 @@ export const useEventTypeForm = ({
381379

382380
// eslint-disable-next-line @typescript-eslint/no-unused-vars
383381
const { availability, users, scheduleName, disabledCancelling, disabledRescheduling, ...rest } = input;
382+
// Strip children down to only the fields the server schema expects.
383+
// The full children objects contain avatar, profile, and other display-only
384+
// data that bloats the request payload. With many assigned users (~85+),
385+
// this can push the request body over the 1MB server limit.
386+
const strippedChildren = children ? stripChildrenForPayload(children) : undefined;
387+
384388
const payload = {
385389
...rest,
386390
length,
@@ -402,7 +406,7 @@ export const useEventTypeForm = ({
402406
seatsShowAvailabilityCount,
403407
metadata,
404408
customInputs,
405-
children,
409+
children: strippedChildren,
406410
assignAllTeamMembers,
407411
multiplePrivateLinks: values.multiplePrivateLinks,
408412
disableCancelling: disabledCancelling,

0 commit comments

Comments
 (0)