Repository navigation
How Markdown Is Retrieved
One call makes one GET request to the Stack Exchange API, version 2.2, and hands back the first item's body_markdown. This page gives the exact request for each option, what comes back for each kind of answer, and when the callback runs. Every request line below is what the local stand-in for api.stackexchange.com received from the published 2.0.0.
The URL is always:
https://api.stackexchange.com/2.2/<questions or answers>/<ids>?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY[&key=<apiKey>]&site=<site>
The parameters come in that order. filter is a fixed API filter that makes the API include body_markdown; nothing else in the URL can be changed, including the API version.
{entityId: 11227809} requests:
GET https://api.stackexchange.com/2.2/questions/11227809?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&site=stackoverflow
{entityId: 1010, isForAnswer: true} requests:
GET https://api.stackexchange.com/2.2/answers/1010?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&site=stackoverflow
site goes into the query as given, short name or domain. {entityId: 127968, site: 'scifi.stackexchange.com'} requests:
GET https://api.stackexchange.com/2.2/questions/127968?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&site=scifi.stackexchange.com
apiKey becomes key, placed before site and escaped. {entityId: 1, apiKey: 'k=y&z w/+'} requests:
GET https://api.stackexchange.com/2.2/questions/1?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&key=k%3Dy%26z%20w%2F%2B&site=stackoverflow
An empty apiKey is left out, and an empty site becomes stackoverflow. An apostrophe is escaped as %27 (1.x did the same), so site: "it's" requests:
GET https://api.stackexchange.com/2.2/questions/1?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&site=it%27s
site and apiKey values that are not strings are written the way 1.x wrote them: a number as its digits (site=123), an object as nothing (site=).
The id goes into the path as written, so a string keeps its leading zeros. {entityId: '011227809'} requests:
GET https://api.stackexchange.com/2.2/questions/011227809?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&site=stackoverflow
Numbers are written with String(), so 2 ** 53 becomes 9007199254740992, while 1e21 (whose string is 1e+21) is refused. Several ids joined by ; go into one request and the call returns the first item in the API's answer. With the stand-in, which lists items in the order asked, {entityId: '2;1'} requested:
GET https://api.stackexchange.com/2.2/questions/2;1?order=asc&filter=!L_(I6pMIzdXP-hC1clc9EY&site=stackoverflow
and resolved to the text of question 2. The real API's item order for such a request was not tested; the package sends order=asc and takes item 0. Ids that are not post ids are refused before any request: see Edge cases and errors.
The package parses the JSON and returns items[0].body_markdown. It does nothing to the text: no rendering, no trimming, and HTML entities such as " stay as the API sent them. What each kind of answer gives, with the callback and without one:
| The API's answer | Callback gets | Promise gives |
|---|---|---|
a post with body_markdown
|
the text | the text |
| no items (no such post) | (null, null) |
null |
a post without body_markdown
|
(undefined, null) |
null |
an empty body_markdown
|
('', null) |
'' |
body_markdown that is a number (42) |
(42, null) |
null |
the JSON literal null
|
(null, null) |
null |
an error body without error_message (status 404) |
(null, null) |
null |
an error_message (any status, even 200 with items) |
(null, StackExchangeError) |
rejects |
| not JSON | (null, StackExchangeError) |
rejects |
The HTTP status alone decides nothing: a 404 without error_message resolves null, and a 200 with error_message fails. From the verification output, a post without body_markdown:
callback: ("<undefined>", null)
promise: resolves null
and an error body without error_message, status 404:
callback: (null, null)
promise: resolves null
The API compresses its answers. fetch undoes a Content-Encoding header itself; the package then also looks at the first bytes of the body and inflates gzip or zlib data that arrived without the header, as 1.x's zlib.unzip did. So all of these gave their text: gzip with the header, deflate with the header, gzip bytes without a header, and plain JSON (which 1.x could not read).
A body the package inflates itself stops at 64 MiB with StackExchangeError: The decompressed response is larger than 67108864 bytes. A body with Content-Encoding: gzip is inflated by fetch before the package sees it, so that limit does not apply to it: 65 MiB of valid JSON sent that way came back whole. The verification output for that case:
resolves a string of 68157440 characters
fetch follows redirects. A 302 from the stand-in to /2.2/questions/1 gave question 1's text after two requests.
The callback always runs after retrieveMarkdown returns, never during the call:
import {retrieveMarkdown} from 'stack-exchange-markdown-retriever';
retrieveMarkdown({entityId: 1}, (markdown, error) => {
console.log('callback:', markdown, error);
});
console.log('retrieveMarkdown returned');prints (sample content):
retrieveMarkdown returned
callback: Sample question one. null
It runs once, in a microtask of its own, outside the Promise chain. That is why an exception thrown in it is uncaught and why it is not called a second time; Edge cases and errors shows the result.
- It keeps no cache and makes one request per call, even for the same id twice.
- It does not track the API's quota (300 requests a day per IP address without a key, as the README says) and does not back off or retry. A throttled request fails with the API's error: see Recipes.
- It reads no environment variables. Proxies are the runtime's business: see Recipes.
This wiki describes stack-exchange-markdown-retriever 2.0.0 and was last updated on 2026-09-29. The library is MIT licensed. Report problems in the issues. On npm: stack-exchange-markdown-retriever.
Using it
The releases
Contributing
Elsewhere