bxo 0.0.5-dev.83 → 0.0.5-dev.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/example/multipart-example.ts +82 -75
- package/example/redirect-documentation.md +298 -0
- package/example/redirect-example.ts +222 -0
- package/package.json +2 -1
- package/src/index.ts +10 -1
- package/test-bun-file.ts +0 -0
- package/test-url-encoding.ts +0 -21
|
@@ -5,107 +5,107 @@ const app = new BXO();
|
|
|
5
5
|
|
|
6
6
|
// Define Zod schema for the form data structure
|
|
7
7
|
const UserFormSchema = z.object({
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
name: z.string(),
|
|
9
|
+
email: z.string().email(),
|
|
10
|
+
password: z.string().min(6),
|
|
11
|
+
is_active: z.string().transform(val => val === "1"),
|
|
12
|
+
profile: z.object({
|
|
13
|
+
name: z.string()
|
|
14
|
+
}),
|
|
15
|
+
id: z.string().optional(),
|
|
16
|
+
created_at: z.string().optional(),
|
|
17
|
+
updated_at: z.string().optional(),
|
|
18
|
+
idx: z.string().transform(val => parseInt(val, 10))
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
// Route to handle multipart/form-data with nested objects
|
|
22
22
|
app.post("/users", async (ctx) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
// The form data is automatically parsed into nested objects
|
|
24
|
+
// ctx.body will contain the structured data based on the schema
|
|
25
|
+
console.log("Parsed form data:", ctx.body);
|
|
26
|
+
|
|
27
|
+
return ctx.json({
|
|
28
|
+
message: "User created successfully",
|
|
29
|
+
data: ctx.body
|
|
30
|
+
});
|
|
31
31
|
}, {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
body: UserFormSchema,
|
|
33
|
+
detail: {
|
|
34
|
+
summary: "Create user with multipart/form-data",
|
|
35
|
+
description: "Handles form data with nested objects and arrays",
|
|
36
|
+
tags: ["Users"]
|
|
37
|
+
}
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
// Example with arrays
|
|
41
41
|
const ArrayFormSchema = z.object({
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
items: z.array(z.string()),
|
|
43
|
+
tags: z.array(z.string()),
|
|
44
|
+
profile: z.object({
|
|
45
|
+
name: z.string(),
|
|
46
|
+
age: z.string().transform(val => parseInt(val, 10))
|
|
47
|
+
})
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
app.post("/items", async (ctx) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
console.log("Parsed array form data:", ctx.body);
|
|
52
|
+
|
|
53
|
+
return ctx.json({
|
|
54
|
+
message: "Items processed successfully",
|
|
55
|
+
data: ctx.body
|
|
56
|
+
});
|
|
57
57
|
}, {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
body: ArrayFormSchema,
|
|
59
|
+
detail: {
|
|
60
|
+
summary: "Process items with arrays",
|
|
61
|
+
description: "Handles form data with arrays like items[0], items[1]",
|
|
62
|
+
tags: ["Items"]
|
|
63
|
+
}
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
// Example with deep nested array objects (like workspace_items[0][id])
|
|
67
67
|
const WorkspaceFormSchema = z.object({
|
|
68
|
-
id: z.string(),
|
|
69
|
-
created_at: z.string(),
|
|
70
|
-
updated_at: z.string(),
|
|
71
|
-
updated_by: z.string(),
|
|
72
|
-
doc_status: z.string().transform(val => parseInt(val, 10)),
|
|
73
|
-
idx: z.string().transform(val => parseInt(val, 10)),
|
|
74
|
-
workspace_items: z.array(z.object({
|
|
75
68
|
id: z.string(),
|
|
76
|
-
type: z.string(),
|
|
77
|
-
value: z.string(),
|
|
78
|
-
options: z.string(),
|
|
79
|
-
workspaceId: z.string(),
|
|
80
|
-
owner: z.string(),
|
|
81
69
|
created_at: z.string(),
|
|
82
70
|
updated_at: z.string(),
|
|
83
|
-
created_by: z.string(),
|
|
84
71
|
updated_by: z.string(),
|
|
85
72
|
doc_status: z.string().transform(val => parseInt(val, 10)),
|
|
86
|
-
|
|
87
|
-
|
|
73
|
+
idx: z.string().transform(val => parseInt(val, 10)),
|
|
74
|
+
workspace_items: z.array(z.object({
|
|
75
|
+
id: z.string(),
|
|
76
|
+
type: z.string(),
|
|
77
|
+
value: z.string(),
|
|
78
|
+
options: z.string(),
|
|
79
|
+
workspaceId: z.string(),
|
|
80
|
+
owner: z.string(),
|
|
81
|
+
created_at: z.string(),
|
|
82
|
+
updated_at: z.string(),
|
|
83
|
+
created_by: z.string(),
|
|
84
|
+
updated_by: z.string(),
|
|
85
|
+
doc_status: z.string().transform(val => parseInt(val, 10)),
|
|
86
|
+
label: z.string()
|
|
87
|
+
}))
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
app.post("/workspace", async (ctx) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
console.log("Parsed workspace form data:", ctx.body);
|
|
92
|
+
|
|
93
|
+
return ctx.json({
|
|
94
|
+
message: "Workspace processed successfully",
|
|
95
|
+
data: ctx.body
|
|
96
|
+
});
|
|
97
97
|
}, {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
body: WorkspaceFormSchema,
|
|
99
|
+
detail: {
|
|
100
|
+
summary: "Process workspace with deep nested array objects",
|
|
101
|
+
description: "Handles form data like workspace_items[0][id], workspace_items[0][type]",
|
|
102
|
+
tags: ["Workspace"]
|
|
103
|
+
}
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
// Test route to show how the parsing works
|
|
107
107
|
app.get("/test-parsing", async (ctx) => {
|
|
108
|
-
|
|
108
|
+
const html = `
|
|
109
109
|
<!DOCTYPE html>
|
|
110
110
|
<html>
|
|
111
111
|
<head>
|
|
@@ -310,10 +310,17 @@ app.get("/test-parsing", async (ctx) => {
|
|
|
310
310
|
</body>
|
|
311
311
|
</html>
|
|
312
312
|
`;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
313
|
+
|
|
314
|
+
return new Response(html, {
|
|
315
|
+
headers: { "Content-Type": "text/html" }
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
app.get("/test-file", async (ctx) => {
|
|
320
|
+
if (!ctx.query.file) {
|
|
321
|
+
return new Response("File not found", { status: 404 });
|
|
322
|
+
}
|
|
323
|
+
return Bun.file("test.txt");
|
|
317
324
|
});
|
|
318
325
|
|
|
319
326
|
app.start();
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Redirect Functionality in BXO
|
|
2
|
+
|
|
3
|
+
BXO provides multiple ways to handle HTTP redirects in your applications. This guide covers all the available methods and best practices.
|
|
4
|
+
|
|
5
|
+
## Available Redirect Methods
|
|
6
|
+
|
|
7
|
+
### 1. Using the `ctx.redirect()` Helper (Recommended)
|
|
8
|
+
|
|
9
|
+
The most convenient way to return redirects is using the built-in `ctx.redirect()` method:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
app.get("/old-page", (ctx) => {
|
|
13
|
+
return ctx.redirect("/new-page"); // Default: 302 status
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
app.get("/permanent-redirect", (ctx) => {
|
|
17
|
+
return ctx.redirect("/new-location", 301); // Permanent redirect
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Using Standard Response with Location Header
|
|
22
|
+
|
|
23
|
+
For more control over headers, use the standard Response API:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
app.get("/custom-redirect", (ctx) => {
|
|
27
|
+
return new Response(null, {
|
|
28
|
+
status: 302,
|
|
29
|
+
headers: {
|
|
30
|
+
"Location": "/target",
|
|
31
|
+
"Cache-Control": "no-cache"
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 3. Using Context Status Method
|
|
38
|
+
|
|
39
|
+
You can also use the existing `ctx.status()` method with manual header setting:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
app.get("/manual-redirect", (ctx) => {
|
|
43
|
+
ctx.set.headers["Location"] = "/target";
|
|
44
|
+
return ctx.status(302, null);
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## HTTP Redirect Status Codes
|
|
49
|
+
|
|
50
|
+
BXO supports all standard HTTP redirect status codes:
|
|
51
|
+
|
|
52
|
+
| Status | Code | Name | Description | Method Preservation |
|
|
53
|
+
|--------|------|------|-------------|-------------------|
|
|
54
|
+
| 301 | 301 | Moved Permanently | Resource has permanently moved | No |
|
|
55
|
+
| 302 | 302 | Found | Temporary redirect (default) | No |
|
|
56
|
+
| 303 | 303 | See Other | Forces GET method after POST | No |
|
|
57
|
+
| 307 | 307 | Temporary Redirect | Preserves original method | Yes |
|
|
58
|
+
| 308 | 308 | Permanent Redirect | Preserves original method | Yes |
|
|
59
|
+
|
|
60
|
+
## Common Use Cases
|
|
61
|
+
|
|
62
|
+
### 1. Page Migration (301)
|
|
63
|
+
```typescript
|
|
64
|
+
app.get("/old-url", (ctx) => {
|
|
65
|
+
return ctx.redirect("/new-url", 301);
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Post-Redirect-Get Pattern (303)
|
|
70
|
+
```typescript
|
|
71
|
+
app.post("/form-submit", (ctx) => {
|
|
72
|
+
// Process form data
|
|
73
|
+
return ctx.redirect("/success", 303); // Forces GET
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. API Endpoint Migration (307)
|
|
78
|
+
```typescript
|
|
79
|
+
app.put("/api/old-endpoint", (ctx) => {
|
|
80
|
+
return ctx.redirect("/api/new-endpoint", 307); // Preserves PUT method
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 4. Conditional Redirects
|
|
85
|
+
```typescript
|
|
86
|
+
app.get("/dashboard", (ctx) => {
|
|
87
|
+
const userType = ctx.cookies.userType;
|
|
88
|
+
|
|
89
|
+
if (userType === "admin") {
|
|
90
|
+
return ctx.redirect("/admin-dashboard", 302);
|
|
91
|
+
} else {
|
|
92
|
+
return ctx.redirect("/user-dashboard", 302);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 5. Query Parameter Preservation
|
|
98
|
+
```typescript
|
|
99
|
+
app.get("/search", (ctx) => {
|
|
100
|
+
const queryString = new URLSearchParams(ctx.query).toString();
|
|
101
|
+
const redirectUrl = queryString ? `/new-search?${queryString}` : "/new-search";
|
|
102
|
+
return ctx.redirect(redirectUrl, 301);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 6. Authentication Redirects
|
|
107
|
+
```typescript
|
|
108
|
+
app.get("/protected", (ctx) => {
|
|
109
|
+
const session = ctx.cookies.session;
|
|
110
|
+
|
|
111
|
+
if (!session) {
|
|
112
|
+
return ctx.redirect("/login", 302);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return ctx.json({ message: "Protected content" });
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Best Practices
|
|
120
|
+
|
|
121
|
+
### 1. Choose the Right Status Code
|
|
122
|
+
- **301**: Use for permanent moves (SEO-friendly)
|
|
123
|
+
- **302**: Use for temporary redirects (default)
|
|
124
|
+
- **303**: Use after POST to prevent resubmission
|
|
125
|
+
- **307/308**: Use for API endpoints to preserve HTTP methods
|
|
126
|
+
|
|
127
|
+
### 2. Handle Relative vs Absolute URLs
|
|
128
|
+
```typescript
|
|
129
|
+
// Relative URL (stays on same domain)
|
|
130
|
+
ctx.redirect("/new-path", 302);
|
|
131
|
+
|
|
132
|
+
// Absolute URL (can redirect to different domain)
|
|
133
|
+
ctx.redirect("https://example.com/new-path", 301);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3. Preserve Query Parameters When Needed
|
|
137
|
+
```typescript
|
|
138
|
+
app.get("/old-search", (ctx) => {
|
|
139
|
+
const { q, page, filters } = ctx.query;
|
|
140
|
+
const params = new URLSearchParams(ctx.query);
|
|
141
|
+
const redirectUrl = params.toString() ? `/new-search?${params}` : "/new-search";
|
|
142
|
+
return ctx.redirect(redirectUrl, 301);
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 4. Add Custom Headers When Necessary
|
|
147
|
+
```typescript
|
|
148
|
+
app.get("/redirect-with-cache", (ctx) => {
|
|
149
|
+
return new Response(null, {
|
|
150
|
+
status: 301,
|
|
151
|
+
headers: {
|
|
152
|
+
"Location": "/new-location",
|
|
153
|
+
"Cache-Control": "public, max-age=3600"
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Testing Redirects
|
|
160
|
+
|
|
161
|
+
### Using curl
|
|
162
|
+
```bash
|
|
163
|
+
# Check redirect status and location
|
|
164
|
+
curl -I http://localhost:3000/old-page
|
|
165
|
+
|
|
166
|
+
# Follow redirects automatically
|
|
167
|
+
curl -L http://localhost:3000/old-page
|
|
168
|
+
|
|
169
|
+
# Test with different HTTP methods
|
|
170
|
+
curl -X POST http://localhost:3000/form-submit
|
|
171
|
+
curl -X PUT http://localhost:3000/api/update
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Using JavaScript fetch
|
|
175
|
+
```javascript
|
|
176
|
+
// Don't follow redirects automatically
|
|
177
|
+
fetch('/old-page', { redirect: 'manual' })
|
|
178
|
+
.then(response => {
|
|
179
|
+
console.log('Status:', response.status);
|
|
180
|
+
console.log('Location:', response.headers.get('Location'));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Follow redirects automatically (default)
|
|
184
|
+
fetch('/old-page')
|
|
185
|
+
.then(response => response.text())
|
|
186
|
+
.then(data => console.log(data));
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Common Patterns
|
|
190
|
+
|
|
191
|
+
### 1. URL Shortening
|
|
192
|
+
```typescript
|
|
193
|
+
app.get("/s/:shortId", (ctx) => {
|
|
194
|
+
const { shortId } = ctx.params;
|
|
195
|
+
const longUrl = getLongUrl(shortId); // Your database lookup
|
|
196
|
+
return ctx.redirect(longUrl, 302);
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 2. Language/Region Redirects
|
|
201
|
+
```typescript
|
|
202
|
+
app.get("/", (ctx) => {
|
|
203
|
+
const acceptLanguage = ctx.headers["accept-language"];
|
|
204
|
+
const region = detectRegion(acceptLanguage);
|
|
205
|
+
return ctx.redirect(`/${region}/home`, 302);
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 3. Maintenance Mode
|
|
210
|
+
```typescript
|
|
211
|
+
app.get("*", (ctx) => {
|
|
212
|
+
if (isMaintenanceMode()) {
|
|
213
|
+
return ctx.redirect("/maintenance", 503);
|
|
214
|
+
}
|
|
215
|
+
// Continue with normal routing
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 4. HTTPS Redirect
|
|
220
|
+
```typescript
|
|
221
|
+
app.get("*", (ctx) => {
|
|
222
|
+
if (ctx.request.url.startsWith("http://") && isProduction()) {
|
|
223
|
+
const httpsUrl = ctx.request.url.replace("http://", "https://");
|
|
224
|
+
return ctx.redirect(httpsUrl, 301);
|
|
225
|
+
}
|
|
226
|
+
// Continue with normal routing
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Error Handling
|
|
231
|
+
|
|
232
|
+
### 1. Invalid Redirect URLs
|
|
233
|
+
```typescript
|
|
234
|
+
app.get("/redirect", (ctx) => {
|
|
235
|
+
const { url } = ctx.query;
|
|
236
|
+
|
|
237
|
+
if (!url || !isValidUrl(url)) {
|
|
238
|
+
return ctx.status(400, { error: "Invalid redirect URL" });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return ctx.redirect(url, 302);
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 2. Redirect Loops Prevention
|
|
246
|
+
```typescript
|
|
247
|
+
app.get("/redirect", (ctx) => {
|
|
248
|
+
const redirectCount = parseInt(ctx.headers["x-redirect-count"] || "0");
|
|
249
|
+
|
|
250
|
+
if (redirectCount > 5) {
|
|
251
|
+
return ctx.status(400, { error: "Too many redirects" });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Add redirect count header
|
|
255
|
+
ctx.set.headers["x-redirect-count"] = (redirectCount + 1).toString();
|
|
256
|
+
return ctx.redirect("/target", 302);
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Performance Considerations
|
|
261
|
+
|
|
262
|
+
1. **Minimize Redirect Chains**: Avoid multiple redirects in sequence
|
|
263
|
+
2. **Use 301 for Permanent Moves**: Helps with SEO and caching
|
|
264
|
+
3. **Consider CDN Caching**: Some CDNs cache redirects based on status codes
|
|
265
|
+
4. **Monitor Redirect Performance**: Track redirect success rates and response times
|
|
266
|
+
|
|
267
|
+
## Security Considerations
|
|
268
|
+
|
|
269
|
+
1. **Validate Redirect URLs**: Prevent open redirect vulnerabilities
|
|
270
|
+
2. **Use HTTPS**: Always redirect to HTTPS in production
|
|
271
|
+
3. **Sanitize User Input**: Clean any user-provided redirect URLs
|
|
272
|
+
4. **Implement Rate Limiting**: Prevent redirect abuse
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
app.get("/redirect", (ctx) => {
|
|
276
|
+
const { url } = ctx.query;
|
|
277
|
+
|
|
278
|
+
// Security: Validate redirect URL
|
|
279
|
+
if (!url || !isAllowedRedirect(url)) {
|
|
280
|
+
return ctx.status(400, { error: "Invalid redirect URL" });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return ctx.redirect(url, 302);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
function isAllowedRedirect(url: string): boolean {
|
|
287
|
+
try {
|
|
288
|
+
const parsedUrl = new URL(url);
|
|
289
|
+
// Only allow redirects to same domain or trusted domains
|
|
290
|
+
return parsedUrl.hostname === "localhost" ||
|
|
291
|
+
parsedUrl.hostname === "example.com";
|
|
292
|
+
} catch {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
This comprehensive guide should help you implement redirects effectively in your BXO applications!
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import BXO from "../src";
|
|
2
|
+
|
|
3
|
+
async function main() {
|
|
4
|
+
const app = new BXO({ serve: { port: 3002 } });
|
|
5
|
+
|
|
6
|
+
// Example 1: Simple temporary redirect (302) - default
|
|
7
|
+
app.get("/old-page", (ctx) => {
|
|
8
|
+
return ctx.redirect("/new-page");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Example 2: Permanent redirect (301)
|
|
12
|
+
app.get("/permanent-redirect", (ctx) => {
|
|
13
|
+
return ctx.redirect("/new-location", 301);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Example 3: Temporary redirect (302) - explicit
|
|
17
|
+
app.get("/temp-redirect", (ctx) => {
|
|
18
|
+
return ctx.redirect("/target", 302);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Example 4: See Other redirect (303) - forces GET method
|
|
22
|
+
app.post("/form-submit", (ctx) => {
|
|
23
|
+
// After POST, redirect to success page with GET
|
|
24
|
+
return ctx.redirect("/success", 303);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Example 5: Temporary redirect preserving method (307)
|
|
28
|
+
app.put("/api/update", (ctx) => {
|
|
29
|
+
// Preserves PUT method in redirect
|
|
30
|
+
return ctx.redirect("/api/updated", 307);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Example 6: Permanent redirect preserving method (308)
|
|
34
|
+
app.patch("/api/patch", (ctx) => {
|
|
35
|
+
// Preserves PATCH method in redirect
|
|
36
|
+
return ctx.redirect("/api/patched", 308);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Example 7: External redirect
|
|
40
|
+
app.get("/external", (ctx) => {
|
|
41
|
+
return ctx.redirect("https://example.com", 301);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Example 8: Conditional redirect based on query parameters
|
|
45
|
+
app.get("/conditional", (ctx) => {
|
|
46
|
+
const { type } = ctx.query;
|
|
47
|
+
|
|
48
|
+
if (type === "admin") {
|
|
49
|
+
return ctx.redirect("/admin-dashboard", 302);
|
|
50
|
+
} else if (type === "user") {
|
|
51
|
+
return ctx.redirect("/user-dashboard", 302);
|
|
52
|
+
} else {
|
|
53
|
+
return ctx.redirect("/default-page", 302);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Example 9: Redirect with path parameters
|
|
58
|
+
app.get("/user/:id", (ctx) => {
|
|
59
|
+
const { id } = ctx.params;
|
|
60
|
+
return ctx.redirect(`/profile/${id}`, 301);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Example 10: Redirect with query string preservation
|
|
64
|
+
app.get("/search", (ctx) => {
|
|
65
|
+
const { q, page } = ctx.query;
|
|
66
|
+
const queryString = new URLSearchParams(ctx.query).toString();
|
|
67
|
+
const redirectUrl = queryString ? `/new-search?${queryString}` : "/new-search";
|
|
68
|
+
return ctx.redirect(redirectUrl, 301);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Example 11: Redirect with custom headers
|
|
72
|
+
app.get("/custom-redirect", (ctx) => {
|
|
73
|
+
// You can still use the standard Response approach for custom headers
|
|
74
|
+
return new Response(null, {
|
|
75
|
+
status: 302,
|
|
76
|
+
headers: {
|
|
77
|
+
"Location": "/target",
|
|
78
|
+
"Cache-Control": "no-cache",
|
|
79
|
+
"X-Custom-Header": "redirect-value"
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Example 12: Redirect after authentication
|
|
85
|
+
app.post("/login", (ctx) => {
|
|
86
|
+
const { username, password } = ctx.body;
|
|
87
|
+
|
|
88
|
+
// Simple authentication check
|
|
89
|
+
if (username === "admin" && password === "password") {
|
|
90
|
+
// Set session cookie
|
|
91
|
+
ctx.set.cookie("session", "authenticated", {
|
|
92
|
+
httpOnly: true,
|
|
93
|
+
maxAge: 3600
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Redirect to dashboard
|
|
97
|
+
return ctx.redirect("/dashboard", 303);
|
|
98
|
+
} else {
|
|
99
|
+
// Redirect back to login with error
|
|
100
|
+
return ctx.redirect("/login?error=invalid-credentials", 303);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Example 13: Redirect with status code validation
|
|
105
|
+
app.get("/validated-redirect", (ctx) => {
|
|
106
|
+
const { status } = ctx.query;
|
|
107
|
+
|
|
108
|
+
// Validate status code
|
|
109
|
+
const validStatuses = [301, 302, 303, 307, 308];
|
|
110
|
+
const redirectStatus = validStatuses.includes(Number(status)) ? Number(status) : 302;
|
|
111
|
+
|
|
112
|
+
return ctx.redirect("/target", redirectStatus as 301 | 302 | 303 | 307 | 308);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Example 14: Redirect with relative and absolute URLs
|
|
116
|
+
app.get("/relative-redirect", (ctx) => {
|
|
117
|
+
// Relative URL redirect
|
|
118
|
+
return ctx.redirect("./relative-path", 302);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
app.get("/absolute-redirect", (ctx) => {
|
|
122
|
+
// Absolute URL redirect
|
|
123
|
+
return ctx.redirect("/absolute-path", 302);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Example 15: Redirect with fragment (hash)
|
|
127
|
+
app.get("/fragment-redirect", (ctx) => {
|
|
128
|
+
return ctx.redirect("/page#section", 302);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Target pages for testing redirects
|
|
132
|
+
app.get("/new-page", (ctx) => {
|
|
133
|
+
return ctx.json({ message: "Welcome to the new page!" });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
app.get("/new-location", (ctx) => {
|
|
137
|
+
return ctx.json({ message: "This is the new permanent location!" });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
app.get("/target", (ctx) => {
|
|
141
|
+
return ctx.json({ message: "Redirect target reached!" });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
app.get("/success", (ctx) => {
|
|
145
|
+
return ctx.json({ message: "Form submitted successfully!" });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
app.get("/dashboard", (ctx) => {
|
|
149
|
+
const session = ctx.cookies.session;
|
|
150
|
+
if (session === "authenticated") {
|
|
151
|
+
return ctx.json({ message: "Welcome to the dashboard!" });
|
|
152
|
+
} else {
|
|
153
|
+
return ctx.redirect("/login", 302);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
app.get("/login", (ctx) => {
|
|
158
|
+
const { error } = ctx.query;
|
|
159
|
+
return ctx.json({
|
|
160
|
+
message: "Login page",
|
|
161
|
+
error: error || null
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
app.get("/new-search", (ctx) => {
|
|
166
|
+
return ctx.json({
|
|
167
|
+
message: "New search page",
|
|
168
|
+
query: ctx.query
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
app.get("/profile/:id", (ctx) => {
|
|
173
|
+
return ctx.json({
|
|
174
|
+
message: `Profile page for user ${ctx.params.id}`
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
app.get("/admin-dashboard", (ctx) => {
|
|
179
|
+
return ctx.json({ message: "Admin dashboard" });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
app.get("/user-dashboard", (ctx) => {
|
|
183
|
+
return ctx.json({ message: "User dashboard" });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
app.get("/default-page", (ctx) => {
|
|
187
|
+
return ctx.json({ message: "Default page" });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
app.get("/page", (ctx) => {
|
|
191
|
+
return ctx.json({ message: "Page with section" });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
app.start();
|
|
195
|
+
console.log(`Redirect example server running on http://localhost:${app.server?.port}`);
|
|
196
|
+
console.log("\nTry these redirect endpoints:");
|
|
197
|
+
console.log("GET /old-page -> /new-page (302)");
|
|
198
|
+
console.log("GET /permanent-redirect -> /new-location (301)");
|
|
199
|
+
console.log("GET /temp-redirect -> /target (302)");
|
|
200
|
+
console.log("POST /form-submit -> /success (303)");
|
|
201
|
+
console.log("PUT /api/update -> /api/updated (307)");
|
|
202
|
+
console.log("PATCH /api/patch -> /api/patched (308)");
|
|
203
|
+
console.log("GET /external -> https://example.com (301)");
|
|
204
|
+
console.log("GET /conditional?type=admin -> /admin-dashboard (302)");
|
|
205
|
+
console.log("GET /conditional?type=user -> /user-dashboard (302)");
|
|
206
|
+
console.log("GET /conditional -> /default-page (302)");
|
|
207
|
+
console.log("GET /user/123 -> /profile/123 (301)");
|
|
208
|
+
console.log("GET /search?q=test&page=1 -> /new-search?q=test&page=1 (301)");
|
|
209
|
+
console.log("GET /custom-redirect -> /target (302) with custom headers");
|
|
210
|
+
console.log("POST /login (admin/password) -> /dashboard (303)");
|
|
211
|
+
console.log("GET /validated-redirect?status=301 -> /target (301)");
|
|
212
|
+
console.log("GET /relative-redirect -> ./relative-path (302)");
|
|
213
|
+
console.log("GET /absolute-redirect -> /absolute-path (302)");
|
|
214
|
+
console.log("GET /fragment-redirect -> /page#section (302)");
|
|
215
|
+
console.log("\nTest with curl:");
|
|
216
|
+
console.log("curl -I http://localhost:3002/old-page");
|
|
217
|
+
console.log("curl -I http://localhost:3002/permanent-redirect");
|
|
218
|
+
console.log("curl -X POST http://localhost:3002/form-submit");
|
|
219
|
+
console.log("curl -X PUT http://localhost:3002/api/update");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
".": "./src/index.ts",
|
|
6
6
|
"./plugins": "./plugins/index.ts"
|
|
7
7
|
},
|
|
8
|
-
"version": "0.0.5-dev.
|
|
8
|
+
"version": "0.0.5-dev.85",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"@types/bun": "latest"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
+
"axios": "^1.12.2",
|
|
14
15
|
"typescript": "^5"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
package/src/index.ts
CHANGED
|
@@ -83,9 +83,10 @@ export type Context<P extends string = string, S extends RouteSchema | undefined
|
|
|
83
83
|
json: <T>(data: T, status?: number) => Response;
|
|
84
84
|
text: (data: string, status?: number) => Response;
|
|
85
85
|
status: <T extends number>(status: T, data: InferResponse<S, T>) => Response;
|
|
86
|
+
redirect: (url: string, status?: 301 | 302 | 303 | 307 | 308) => Response;
|
|
86
87
|
};
|
|
87
88
|
|
|
88
|
-
type AnyHandler = (ctx: Context<any, any>, app: BXO) => Response | string | Promise<Response | string>;
|
|
89
|
+
type AnyHandler = (ctx: Context<any, any>, app: BXO) => Response | string | BunFile | Promise<Response | string | BunFile>;
|
|
89
90
|
type Handler<P extends string, S extends RouteSchema | undefined = undefined> = (
|
|
90
91
|
ctx: Context<P, S>,
|
|
91
92
|
app: BXO
|
|
@@ -757,6 +758,14 @@ export default class BXO {
|
|
|
757
758
|
}
|
|
758
759
|
|
|
759
760
|
return toResponse(data, { status });
|
|
761
|
+
},
|
|
762
|
+
redirect: (url, status = 302) => {
|
|
763
|
+
return new Response(null, {
|
|
764
|
+
status,
|
|
765
|
+
headers: {
|
|
766
|
+
"Location": url
|
|
767
|
+
}
|
|
768
|
+
});
|
|
760
769
|
}
|
|
761
770
|
};
|
|
762
771
|
|
package/test-bun-file.ts
DELETED
|
File without changes
|
package/test-url-encoding.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import BXO from "./src";
|
|
2
|
-
|
|
3
|
-
const app = new BXO({ serve: { port: 3001 } });
|
|
4
|
-
|
|
5
|
-
// Test the exact scenario you mentioned
|
|
6
|
-
app.get("/api/resources/:resourceType/:id", (ctx) => {
|
|
7
|
-
return ctx.json({
|
|
8
|
-
message: "Success!",
|
|
9
|
-
resourceType: ctx.params.resourceType,
|
|
10
|
-
id: ctx.params.id,
|
|
11
|
-
allParams: ctx.params,
|
|
12
|
-
url: ctx.request.url,
|
|
13
|
-
pathname: new URL(ctx.request.url).pathname
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
app.start();
|
|
18
|
-
console.log(`Test server running on http://localhost:${app.server?.port}`);
|
|
19
|
-
console.log(`Test URL: http://localhost:${app.server?.port}/api/resources/Doctype%20Permission/01992af8-1c69-7000-9219-9b83c2feb2d6`);
|
|
20
|
-
|
|
21
|
-
|