Skip to content
Open
Show file tree
Hide file tree
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
94 changes: 94 additions & 0 deletions reference/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Test file for middleware redirect logic.
* This can be run locally to verify the case-insensitive redirect behavior.
*/

/**
* Simple test cases to verify the middleware logic.
*/
const testCases = [
// Test uppercase in path
{
input: '/python/LangChain/index.html',
expected: '/python/langchain/index.html',
shouldRedirect: true,
},
// Test mixed case
{
input: '/python/LangGraph/Graphs',
expected: '/python/langgraph/graphs',
shouldRedirect: true,
},
// Test all lowercase (no redirect needed)
{
input: '/python/langchain/index.html',
expected: '/python/langchain/index.html',
shouldRedirect: false,
},
// Test JavaScript paths
{
input: '/javascript/LangChain/Agents',
expected: '/javascript/langchain/agents',
shouldRedirect: true,
},
// Test complex path with query params
{
input: '/python/Integrations/LangChain_OpenAI',
expected: '/python/integrations/langchain_openai',
shouldRedirect: true,
},
];

/**
* Simulate the middleware logic for testing.
*/
function simulateMiddleware(pathname: string): { shouldRedirect: boolean; newPath: string } {
const shouldRedirect = pathname !== pathname.toLowerCase();
return {
shouldRedirect,
newPath: pathname.toLowerCase(),
};
}

/**
* Run tests and log results.
*/
function runTests(): void {
console.log('Running middleware tests...\n');

let passed = 0;
let failed = 0;

for (const testCase of testCases) {
const result = simulateMiddleware(testCase.input);
const testPassed =
result.shouldRedirect === testCase.shouldRedirect &&
result.newPath === testCase.expected;

if (testPassed) {
passed++;
console.log(`✓ PASS: ${testCase.input}`);
} else {
failed++;
console.log(`✗ FAIL: ${testCase.input}`);
console.log(` Expected redirect: ${testCase.shouldRedirect}, got: ${result.shouldRedirect}`);
console.log(` Expected path: ${testCase.expected}, got: ${result.newPath}`);
}
}

console.log(`\nResults: ${passed} passed, ${failed} failed`);

if (failed === 0) {
console.log('All tests passed! ✓');
} else {
console.error('Some tests failed! ✗');
process.exit(1);
}
}

// Run tests if this file is executed directly
if (require.main === module) {
Copy link

Copilot AI Oct 13, 2025

Choose a reason for hiding this comment

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

Using require.main === module is a CommonJS pattern, but this file is configured as ESNext module. Consider using a more modern approach like checking import.meta.main or restructuring to avoid this Node.js-specific check.

Suggested change
if (require.main === module) {
if (import.meta.url === `file://${process.argv[1]}`) {

Copilot uses AI. Check for mistakes.

runTests();
}

export { testCases, simulateMiddleware, runTests };
33 changes: 33 additions & 0 deletions reference/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from 'next/server';

/**
* Middleware to handle case-insensitive redirects for API reference paths.
* Redirects any path containing uppercase letters to its lowercase equivalent.
*/
export function middleware(request: NextRequest): NextResponse {
const { pathname, search } = request.nextUrl;

if (pathname !== pathname.toLowerCase()) {
const url = request.nextUrl.clone();
url.pathname = pathname.toLowerCase();

// Preserve query parameters
url.search = search;

Comment on lines +14 to +16
Copy link

Copilot AI Oct 13, 2025

Choose a reason for hiding this comment

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

[nitpick] The comment about preserving query parameters is redundant since the search parameter is already extracted from request.nextUrl and reassigned. The clone() method already preserves the search parameters, making this assignment unnecessary unless you're intentionally overriding them.

Suggested change
// Preserve query parameters
url.search = search;

Copilot uses AI. Check for mistakes.

// 301 permanent redirect to the lowercase version
return NextResponse.redirect(url, { status: 301 });
}

return NextResponse.next();
}

/**
* Configure which paths the middleware should run on.
* This applies to all Python API reference paths.
*/
export const config = {
matcher: [
'/python/:path*',
'/javascript/:path*',
],
};
9 changes: 8 additions & 1 deletion reference/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@
"build": "concurrently \"pnpm build:js\" \"pnpm build:py\"",
"build:js": "pnpm -C ./javascript build",
"build:py": "make -C ./python build",
"preview": "vercel dev"
"preview": "vercel dev",
"test:middleware": "tsx middleware.test.ts"
},
"packageManager": "pnpm@10.14.0",
"dependencies": {
"concurrently": "^9.2.1",
"serve": "^14.2.5",
"vercel": "^48.1.4"
},
"devDependencies": {
"@types/node": "^22.0.0",
"next": "^15.1.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
}
18 changes: 18 additions & 0 deletions reference/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"types": ["node"]
},
"include": ["middleware.ts", "middleware.test.ts"],
"exclude": ["node_modules", "dist", "python", "javascript"]
}