🚮 fix: Replace Backtracking LangChain Error Regexes (#15473)

This commit is contained in:
Danny Avila 2026-09-01 16:52:48 -04:00 • committed by GitHub
parent 6122d210da
commit 0f585497ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 137 additions and 6 deletions

View file

@ -16,6 +16,12 @@ describe('stripLangChainTroubleshootingUrl', () => {
expect(stripLangChainTroubleshootingUrl(message)).toBe('outer inner');
});
it('removes through the final classification when one URL carries more than one error path', () => {
const message =
'failed Troubleshooting URL: https://docs.langchain.com/errors/FIRST/wrapped/errors/MODEL_NOT_FOUND/';
expect(stripLangChainTroubleshootingUrl(message)).toBe('failed');
});
it('leaves provider text without the tail untouched', () => {
const message = '400 Bad Request\nRequest contains an invalid argument';
expect(stripLangChainTroubleshootingUrl(message)).toBe(message);
@ -29,6 +35,14 @@ describe('stripLangChainTroubleshootingUrl', () => {
it.each([undefined, null])('treats an absent message as empty', (message) => {
expect(stripLangChainTroubleshootingUrl(message)).toBe('');
});
it('handles adversarial non-matching URLs without regex backtracking', () => {
const message = `Troubleshooting URL: https://${'langchain/'.repeat(8_000)}missing`;
const startedAt = Date.now();
expect(stripLangChainTroubleshootingUrl(message)).toBe(message);
expect(Date.now() - startedAt).toBeLessThan(500);
});
});
describe('parseLangChainErrorCode', () => {
@ -39,10 +53,23 @@ describe('parseLangChainErrorCode', () => {
},
);
it('reads the final classification from a URL that carries more than one error path', () => {
const message = 'https://docs.langchain.com/errors/FIRST/wrapped/errors/MODEL_NOT_FOUND/';
expect(parseLangChainErrorCode(message)).toBe('MODEL_NOT_FOUND');
});
it.each(['429 Too Many Requests', '', undefined, { message: 'nested' }])(
'returns undefined when the text carries no classification',
(message) => {
expect(parseLangChainErrorCode(message)).toBeUndefined();
},
);
it('handles adversarial non-matching URLs without regex backtracking', () => {
const message = `https://${'langchain/'.repeat(8_000)}missing`;
const startedAt = Date.now();
expect(parseLangChainErrorCode(message)).toBeUndefined();
expect(Date.now() - startedAt).toBeLessThan(500);
});
});

View file

@ -4,10 +4,10 @@
* because the server strips the URL before persisting the message, while the client still reads the
* code back out of messages persisted before it did.
*/
const TROUBLESHOOTING_URL =
/\s*Troubleshooting URL:\s*https?:\/\/\S*langchain\S*\/errors\/[A-Za-z_]+\/?\s*/g;
const ERROR_CODE_URL = /langchain\S*\/errors\/([A-Za-z_]+)/i;
const LANGCHAIN = 'langchain';
const ERROR_PATH = '/errors/';
const TROUBLESHOOTING_LABEL = 'Troubleshooting URL:';
const WHITESPACE = /\s/;
/** Provider errors cross untyped boundaries, so a `message` is not guaranteed to be a string. */
function toMessageText(message: unknown): string {
@ -17,12 +17,116 @@ function toMessageText(message: unknown): string {
return message == null ? '' : String(message);
}
function isErrorCodeCharacter(character: string): boolean {
const code = character.charCodeAt(0);
return character === '_' || (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
}
function findTokenEnd(text: string, start: number, limit = text.length): number {
let end = start;
while (end < limit && !WHITESPACE.test(text[end])) {
end += 1;
}
return end;
}
function findErrorCode(
text: string,
searchableText: string,
start: number,
end: number,
): { start: number; end: number } | undefined {
const langChainIndex = searchableText.indexOf(LANGCHAIN, start);
if (langChainIndex < 0 || langChainIndex >= end) {
return undefined;
}
let errorCode: { start: number; end: number } | undefined;
let pathIndex = searchableText.indexOf(ERROR_PATH, langChainIndex + LANGCHAIN.length);
while (pathIndex >= 0 && pathIndex < end) {
const codeStart = pathIndex + ERROR_PATH.length;
let codeEnd = codeStart;
while (codeEnd < end && isErrorCodeCharacter(text[codeEnd])) {
codeEnd += 1;
}
if (codeEnd > codeStart) {
errorCode = { start: codeStart, end: codeEnd };
}
pathIndex = searchableText.indexOf(ERROR_PATH, Math.max(codeStart, codeEnd));
}
return errorCode;
}
/** Removes LangChain's appended docs URL so provider text carries no third-party attribution. */
export function stripLangChainTroubleshootingUrl(message: unknown): string {
return toMessageText(message).replace(TROUBLESHOOTING_URL, ' ').trim();
const text = toMessageText(message);
const parts: string[] = [];
let copiedUntil = 0;
let searchFrom = 0;
while (searchFrom < text.length) {
const labelStart = text.indexOf(TROUBLESHOOTING_LABEL, searchFrom);
if (labelStart < 0) {
break;
}
let matchStart = labelStart;
while (matchStart > copiedUntil && WHITESPACE.test(text[matchStart - 1])) {
matchStart -= 1;
}
let urlStart = labelStart + TROUBLESHOOTING_LABEL.length;
while (urlStart < text.length && WHITESPACE.test(text[urlStart])) {
urlStart += 1;
}
if (!text.startsWith('https://', urlStart) && !text.startsWith('http://', urlStart)) {
searchFrom = urlStart;
continue;
}
const urlEnd = findTokenEnd(text, urlStart);
const errorCode = findErrorCode(text, text, urlStart, urlEnd);
if (errorCode == null) {
searchFrom = urlEnd;
continue;
}
let matchEnd = errorCode.end;
if (text[matchEnd] === '/') {
matchEnd += 1;
}
while (matchEnd < text.length && WHITESPACE.test(text[matchEnd])) {
matchEnd += 1;
}
parts.push(text.slice(copiedUntil, matchStart), ' ');
copiedUntil = matchEnd;
searchFrom = matchEnd;
}
parts.push(text.slice(copiedUntil));
return parts.join('').trim();
}
/** The classification LangChain encoded in the docs URL it appended, when the text carries one. */
export function parseLangChainErrorCode(message: unknown): string | undefined {
return toMessageText(message).match(ERROR_CODE_URL)?.[1]?.toUpperCase();
const text = toMessageText(message);
const searchableText = text.toLowerCase();
let searchFrom = 0;
while (searchFrom < text.length) {
const langChainIndex = searchableText.indexOf(LANGCHAIN, searchFrom);
if (langChainIndex < 0) {
return undefined;
}
const tokenEnd = findTokenEnd(text, langChainIndex);
const errorCode = findErrorCode(text, searchableText, langChainIndex, tokenEnd);
if (errorCode != null) {
return text.slice(errorCode.start, errorCode.end).toUpperCase();
}
searchFrom = tokenEnd + 1;
}
return undefined;
}