Skip to content
Merged
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
8 changes: 4 additions & 4 deletions .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ jobs:

strategy:
matrix:
node_version: [22.x, 24.x, 25.x]
node_version: [22.x, 24.x, 26.x]

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7

- name: setup Node.js v${{ matrix.node_version }}
uses: actions/setup-node@v6
uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node_version }}

Expand All @@ -29,7 +29,7 @@ jobs:
npm run test

- name: cache node modules
uses: actions/cache@v5
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
Expand Down
101 changes: 36 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Load and extract oembed data.
```ts
extract(String url)
extract(String url, Object params)
extract(String url, Object params, Object fetchOptions)
extract(String url, Object params, Function fetcher)
```

#### Parameters
Expand All @@ -76,103 +76,74 @@ Here are several popular params:
Note that some params are supported by these providers but not by the others.
Please see the provider's oEmbed API docs carefully for exact information.

##### `fetchOptions` *optional*
##### `fetcher` *optional*

`fetchOptions` is an object that can have the following properties:
A custom fetch function with the signature `(url: string) => Promise<Response>`.
Use this to customize HTTP behavior: proxy, headers, TLS, authentication, timeouts, etc.

- `headers`: to set request headers
- `proxy`: another endpoint to forward the request to
- `agent`: a HTTP proxy agent
- `signal`: AbortController signal or AbortSignal timeout to terminate the request
Defaults to `globalThis.fetch`.

You can use this param to set request headers to fetch.

For example:
**Node.js** (with proxy via undici):

```js
import { extract } from '@extractus/oembed-extractor'
import { fetch, ProxyAgent } from 'undici'

const url = 'https://codepen.io/ndaidong/pen/LYmLKBw'
extract(url, null, {
headers: {
'user-agent': 'Opera/9.60 (Windows NT 6.0; U; en) Presto/2.1.1'
}
})
```
const dispatcher = new ProxyAgent('http://proxy.example.com:8080')
const myFetcher = (url) => fetch(url, { dispatcher })

You can also specify a proxy endpoint to load remote content, instead of fetching directly.
const result = await extract('https://www.youtube.com/watch?v=x2bqscVkGxk', {}, myFetcher)
```

For example:
**Bun** (with proxy):

```js
import { extract } from '@extractus/oembed-extractor'

const url = 'https://codepen.io/ndaidong/pen/LYmLKBw'
extract(url, null, {
headers: {
'user-agent': 'Opera/9.60 (Windows NT 6.0; U; en) Presto/2.1.1'
},
const myFetcher = (url) => fetch(url, {
proxy: {
target: 'https://your-secret-proxy.io/loadJson?url=',
headers: {
'Proxy-Authorization': 'Bearer YWxhZGRpbjpvcGVuc2VzYW1l...'
}
}
url: 'http://proxy.example.com:8080',
},
})
```

With the above setting, request will be forwarded to `https://your-secret-proxy.io/loadJson?url={OEMBED_ENDPOINT}`.
const result = await extract('https://www.youtube.com/watch?v=x2bqscVkGxk', {}, myFetcher)
```

Another way to work with proxy is use `agent` option instead of `proxy` as below:
**Deno** (with proxy):

```js
import { extract } from '@extractus/oembed-extractor'

import { HttpsProxyAgent } from 'https-proxy-agent'

const proxy = 'http://abc:RaNdoMpasswORd_country-France@proxy.packetstream.io:31113'

const url = 'https://codepen.io/ndaidong/pen/LYmLKBw'
import { extract } from 'npm:@extractus/oembed-extractor'

const oembed = await extract(url, null, {
agent: new HttpsProxyAgent(proxy),
const client = Deno.createHttpClient({
proxy: { url: 'http://localhost:8080' },
})
console.log('Run oembed-extractor with proxy:', proxy)
console.log(oembed)
```

For more info about [https-proxy-agent](https://www.npmjs.com/package/https-proxy-agent), check [its repo](https://github.com/TooTallNate/proxy-agents).
const myFetcher = (url) => fetch(url, { client })

By default, there is no request timeout. You can use the option `signal` to cancel request at the right time.
const result = await extract('https://www.youtube.com/watch?v=x2bqscVkGxk', {}, myFetcher)
```

The common way is to use AbortControler:
**Custom headers**:

```js
const controller = new AbortController()

// stop after 5 seconds
setTimeout(() => {
controller.abort()
}, 5000)

const oembed = await extract(url, null, {
signal: controller.signal,
const myFetcher = (url) => fetch(url, {
headers: {
'user-agent': 'MyBot/1.0',
'authorization': 'Bearer token123',
},
})

const result = await extract(url, {}, myFetcher)
```

A newer solution is AbortSignal's `timeout()` static method:
**Request timeout**:

```js
// stop after 5 seconds
const oembed = await extract(url, null, {
const myFetcher = (url) => fetch(url, {
signal: AbortSignal.timeout(5000),
})
```

For more info:

- [AbortController constructor](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
- [AbortSignal: timeout() static method](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static)
const result = await extract(url, {}, myFetcher)
```


### `.setProviderList()`
Expand Down
28 changes: 5 additions & 23 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,38 +135,20 @@ export interface Params {
}

/**
* Configuration for proxy-based requests.
* Custom fetch function. Receives a URL and returns a Response promise.
* Use this to customize HTTP behavior (proxy, headers, TLS, etc.).
*/
export interface ProxyConfig {
/** Base URL of the proxy server */
target?: string
/** Headers to send to the proxy (e.g. Proxy-Authorization) */
headers?: Record<string, string>
}

/**
* Advanced fetch options for extract().
*/
export interface FetchOptions {
/** Custom request headers */
headers?: Record<string, string>
/** Proxy configuration */
proxy?: ProxyConfig
/** HTTP proxy agent (e.g. HttpsProxyAgent) */
agent?: object
/** AbortSignal to cancel the request */
signal?: AbortSignal
}
export type Fetcher = (url: string) => Promise<Response>

/**
* Extract oEmbed data from a given URL.
*
* @param url - URL of a valid oEmbed resource
* @param params - Optional parameters (maxwidth, maxheight, etc.)
* @param fetchOptions - Advanced fetch options (headers, proxy, agent, signal)
* @param fetcher - Custom fetch function. Defaults to globalThis.fetch.
* @returns Promise resolving to oEmbed data
*/
export function extract(url: string, params?: Params, fetchOptions?: FetchOptions): Promise<OembedData>
export function extract(url: string, params?: Params, fetcher?: Fetcher): Promise<OembedData>

/**
* Check if a URL is supported by any registered provider.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "4.1.0",
"version": "5.0.0",
"name": "@extractus/oembed-extractor",
"description": "Get oEmbed data from given URL.",
"homepage": "https://github.com/extractus/oembed-extractor",
Expand All @@ -12,7 +12,7 @@
"type": "module",
"types": "./index.d.ts",
"engines": {
"node": ">= 20"
"node": ">= 22"
},
"scripts": {
"lint": "eslint .",
Expand Down
8 changes: 4 additions & 4 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ import { getEndpoint } from './utils/provider.js'
*
* @param {string} url - URL of a valid oEmbed resource
* @param {object} [params] - Optional parameters (maxwidth, maxheight, theme, lang, etc.)
* @param {object} [options] - Fetch options (headers, proxy, agent, signal)
* @param {Function} [fetcher] - Custom fetch function (url) => Promise<Response>. Defaults to globalThis.fetch.
* @returns {Promise<object>} oEmbed data object
* @throws {Error} If URL is invalid
*/
export const extract = async (url, params = {}, options = {}) => {
export const extract = async (url, params = {}, fetcher = globalThis.fetch) => {
if (!isValidURL(url)) {
throw new Error('Invalid input URL')
}
const endpoint = getEndpoint(url)

return endpoint
? fetchEmbed(url, params, endpoint, options)
: extractWithDiscovery(url, params, options)
? fetchEmbed(url, params, endpoint, fetcher)
: extractWithDiscovery(url, params, fetcher)
}

export {
Expand Down
18 changes: 0 additions & 18 deletions src/main.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import { describe, it } from 'node:test'
import assert from 'node:assert'

import { HttpsProxyAgent } from 'https-proxy-agent'

import nock from 'nock'

import {
Expand All @@ -14,9 +12,6 @@ import {
setProviderList
} from './main.js'

const env = process.env || {}
const PROXY_SERVER = env.PROXY_SERVER || ''

const required = [
'type',
'version',
Expand Down Expand Up @@ -207,19 +202,6 @@ describe('test if extract() with some popular providers', () => {
})
})

if (PROXY_SERVER !== '') {
describe('test extract live oembed API via proxy server', () => {
it('check if extract method works with proxy server', async () => {
const url = 'https://codepen.io/ndaidong/pen/LYmLKBw'
const result = await extract(url, {}, {
agent: new HttpsProxyAgent(PROXY_SERVER),
})
console.log(result)
assert.ok(result.success)
}, 10000)
})
}

it('test .hasProvider() method', () => {
assert.ok(hasProvider('https://www.youtube.com/watch?v=ciS8aCrX-9s'))
assert.ok(!hasProvider('https://trello.com/b/BO3bg7yn/notes'))
Expand Down
8 changes: 4 additions & 4 deletions src/utils/autoDiscovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import { getHtml, getJson } from './retrieve.js'
*
* @param {string} url - Resource URL to discover oEmbed for
* @param {object} [params={}] - Additional oEmbed query parameters
* @param {object} [options={}] - Fetch options (headers, proxy, agent, signal)
* @param {Function} fetcher - Custom fetch function (url) => Promise<Response>
* @returns {Promise<object>} oEmbed response data
* @throws {Error} If no oEmbed link tag is found in the HTML
*/
export default async (url, params = {}, options = {}) => {
const html = await getHtml(url, options)
export default async (url, params = {}, fetcher) => {
const html = await getHtml(url, fetcher)
const doc = new DOMParser().parseFromString(html, 'text/html')
const elm = doc.querySelector('link[type="application/json+oembed"]')
if (!elm) {
Expand All @@ -30,7 +30,7 @@ export default async (url, params = {}, options = {}) => {
}
})
const link = `${origin}${pathname}?${searchParams.toString()}`
const body = await getJson(link, options)
const body = await getJson(link, fetcher)
body.method = 'auto-discovery'
return body
}
2 changes: 1 addition & 1 deletion src/utils/autoDiscovery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('test if autoDiscovery() works correctly', () => {
'Content-Type': 'application/json',
})

const result = await autoDiscovery(url, params)
const result = await autoDiscovery(url, params, globalThis.fetch)
assert.ok(result)
nock.cleanAll()
})
Expand Down
6 changes: 3 additions & 3 deletions src/utils/fetchEmbed.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ const getFacebookGraphToken = () => {
* @param {string} url - Original resource URL
* @param {object} [params={}] - oEmbed parameters (maxwidth, maxheight, etc.)
* @param {string} [endpoint=''] - Provider oEmbed API endpoint
* @param {object} [options={}] - Fetch options (headers, proxy, agent, signal)
* @param {Function} fetcher - Custom fetch function (url) => Promise<Response>
* @returns {Promise<object>} oEmbed response data
*/
export default async (url, params = {}, endpoint = '', options = {}) => { // eslint-disable-line
export default async (url, params = {}, endpoint = '', fetcher) => { // eslint-disable-line
const query = {
url,
format: 'json',
Expand All @@ -54,7 +54,7 @@ export default async (url, params = {}, endpoint = '', options = {}) => { // esl

const queryParams = new URLSearchParams(query).toString()
const link = endpoint + '?' + queryParams
const body = await getJson(link, options)
const body = await getJson(link, fetcher)
body.method = 'provider-api'
return body
}
2 changes: 1 addition & 1 deletion src/utils/fetchEmbed.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ describe('test if fetchEmbed() works correctly', () => {
maxheight = 0,
} = params

const result = await fetchEmbed(url, { maxwidth, maxheight }, endpoint)
const result = await fetchEmbed(url, { maxwidth, maxheight }, endpoint, globalThis.fetch)
assert.ok(result)
assert.equal(result.provider_name, expected.provider_name)
assert.equal(result.type, expected.type)
Expand Down
Loading
Loading