encodeURIComponent won't save you from apostrophes in OData literals
encodeURIComponent doesn't escape the apostrophe, so an apostrophe inside an OData literal must be doubled ('') first — otherwise names like O'Brien and labels like Q1'26 400 the request.
encodeURIComponent apostrof neescapuje, takže apostrof uvnitř OData literálu musíš nejdřív zdvojit ('') – jinak jména jako O'Brien a označení jako Q1'26 shodí požadavek na 400.
Symptom
REST calls work for months, then fail with HTTP 400 for some inputs:
- uploading
Q1'26 report.xlsxvia/Files/add(url='…') - filtering
$filter=Title eq 'O'Brien' getbytitle('Team's list')
The error is a generic bad request or a query-parsing complaint, and the code dutifully calls encodeURIComponent on everything.
Cause
Two different escaping layers are in play, and encodeURIComponent only handles one of them:
- URL encoding — what
encodeURIComponentdoes. But per its spec it does not encode the apostrophe ('passes through untouched). - OData string literals — delimited by apostrophes. An apostrophe inside the value must be escaped by doubling it (
''), exactly like in SQL.
So O'Brien arrives as eq 'O'Brien' — the literal ends after O, and the parser chokes on the rest.
Fix
Escape the OData layer first, then URL-encode:
const odataString = (s: string): string => s.replace(/'/g, "''");
const folderUrl = '/sites/projects/Shared Documents';
const name = "Q1'26 report.xlsx";
const url = `${webUrl}/_api/web/GetFolderByServerRelativePath(decodedurl='${encodeURIComponent(odataString(folderUrl))}')` +
`/Files/AddUsingPath(DecodedUrl='${encodeURIComponent(odataString(name))}',Overwrite=true)`;
const filter = `$filter=Title eq '${encodeURIComponent(odataString(userInput))}'`;
Run odataString() on every dynamic value that lands between OData quotes: getbytitle(…), GetList(@u) parameter aliases, $filter, GetFileByServerRelativePath(decodedurl=…), GetFolderByServerRelativePath(decodedurl=…), …UsingPath(DecodedUrl=…), sitegroups/getbyname(…), and AttachmentFiles/getByFileName(…). A convenient wrapper that does both layers at once: const spLit = (p) => encodeURIComponent(p.replace(/'/g, "''"));.
For file and folder paths, use the ResourcePath calls shown above rather than Files/add(url=…), GetFileByServerRelativeUrl(…) or GetFolderByServerRelativeUrl(…): with a # or % in the name the classic calls cannot find the item even when the path is encoded, and Files/add with an encoded % saves the file under a name containing a literal %25 — A # or % in a file or folder name.
Notes
- This is the classic "works until a customer named O'Brien shows up" bug — test data rarely contains apostrophes; real names, file names and quarter labels do.
- The doubling rule applies to the OData literal only. Don't double apostrophes in JSON request bodies — those are plain JSON strings.
- The opposite mistake — doubling the apostrophes but skipping
encodeURIComponent— breaks on#, which starts the URL fragment and cuts the query off. Guest UPNs (#EXT#) always contain one: An unencoded#in a$filtervalue cuts the URL.