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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [2.4.1-beta-1] - 2026-06-26

### Fixed
- Resolve external `$ref` (HTTP/HTTPS URLs) instead of crashing with `TypeError`. External schemas are fetched, cached, and inlined at parse time using Node's built-in `http`/`https` modules.


## [2.4.0] - 2026-05-07

### Added
Expand Down
14 changes: 9 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,13 @@ try {
require('./src/utils/error.js')('use argv: -c/--configuration. configuration file path does not exist or is not correct: ' + argv.configuration);
}

;(async () => {
global.definition = require('./src/parser/definition.js')()
const version = require('./src/parser/version.js')()
global.environmentVariables = {}
global.testParams = {}
global.configurationFile = configurationFile
require('./src/parser/'+version+'/refs.js')()
await require('./src/parser/'+version+'/refs.js')()

const schemaHostBasePath = require('./src/parser/'+version+'/schemaHostBasePath.js')()
const endpointsParsed = require('./src/parser/endpoints.js')()
Expand Down Expand Up @@ -231,8 +232,8 @@ _.forEach(environments, function (element) {
}
if ( element.custom_authorizations_file ) {
require('./src/parser/authorizationRequests.js')(endpointsStage,element.custom_authorizations_file)
} else if(global.definition.components.securitySchemes){
let securityDefinition = require('./src/parser/openapiAuthorizationDefinition.js')(global.definition.components.securitySchemes)
} else if(globalThis.definition.components?.securitySchemes){
let securityDefinition = require('./src/parser/openapiAuthorizationDefinition.js')(globalThis.definition.components.securitySchemes)
if(securityDefinition){
require('./src/parser/authorizationRequests.js')(endpointsStage,null,securityDefinition)
}
Expand Down Expand Up @@ -299,10 +300,13 @@ function addLettersToName(collection) {
// Añade una letra al nombre de cada Test Case, justo despues del status code. Ej.: 200a OK
// Controla el exceso de Test Cases y añade dos letras en caso de ser necesario. Ej.: 200aa OK, 200ab OK
for (let k in array) {
array[k].name = _.replace(array[k].name, array[k].aux.status,
array[k].name = _.replace(array[k].name, array[k].aux.status,
k < alphabet.length ? array[k].aux.status + alphabet[k] : array[k].aux.status + alphabet[Math.floor(k / alphabet.length) - 1] + alphabet[k % alphabet.length]);
}
}
}
}
}
}
})().catch(err => {
require('./src/utils/error.js')(err.message || String(err))
})
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openapi2postman",
"version": "2.4.0",
"version": "2.4.1-beta-1",
"description": "openapi2postman",
"bin": {
"o2p": "index.js"
Expand Down
2 changes: 1 addition & 1 deletion seeds/parserInitialGoodOpenApi3.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"servers":[
{
"url":"http://petstore.swagger.io/v1"
"url":"https://petstore.swagger.io/v1"
}
],
"paths":{
Expand Down
40 changes: 11 additions & 29 deletions src/parser/openapi3/refs.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,18 @@

'use strict'

function eachRecursive(obj) {
for (let k in obj) {
if (typeof obj[k] == "object" && obj[k] !== null) {
eachRecursive(obj[k]);
} else if (k == '$ref') {
const siblings = {}
for (const sib of Object.keys(obj)) {
if (sib !== '$ref') {
siblings[sib] = obj[sib]
}
}
let property = obj[k]
property = property.replace('#/', '')
let propertiesArray = property.split('/')
let refObject = findObject(globalThis.definition, propertiesArray)
delete obj[k]
Object.assign(obj, refObject, siblings)
}
}
}
const createRefsModule = require('../refs')

function findObject(obj, propertiesArray) {
if(propertiesArray.length < 1) {
return obj
function collectSiblings(obj) {
const siblings = {}
for (const sib of Object.keys(obj)) {
if (sib !== '$ref') siblings[sib] = obj[sib]
}

let property = propertiesArray.shift()
return findObject(obj[property], propertiesArray)
return siblings
}

const resolveRefs = createRefsModule({ prepareSiblings: collectSiblings })

function liftAdditionalOperations() {
if (!globalThis.definition.paths) return
for (const path in globalThis.definition.paths) {
Expand All @@ -46,9 +28,9 @@ function liftAdditionalOperations() {
}

module.exports = function() {
return function get() {
eachRecursive(globalThis.definition)
return async function get() {
await resolveRefs(globalThis.definition, globalThis.definition)
liftAdditionalOperations()
return globalThis.definition
}
}()
}()
103 changes: 103 additions & 0 deletions src/parser/refs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/** Part of APIAddicts. See LICENSE fileor full copyright and licensing details. Supported by Madrid Digital and CloudAPPi **/

'use strict'

const https = require('node:https')
const http = require('node:http')
const yaml = require('js-yaml')
const error = require('../utils/error')

function parseYamlResponse(data, url, resolve, reject) {
try { resolve(yaml.load(data)) }
catch (e) { reject(new Error('Failed to parse external ref from ' + url + ': ' + e.message)) }
}

function fetchUrl(url) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https://') ? https : http
client.get(url, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
return fetchUrl(res.headers.location).then(resolve, reject)
}
if (res.statusCode < 200 || res.statusCode >= 300) {
res.resume()
return reject(new Error('Failed to fetch external ref ' + url + ': HTTP ' + res.statusCode))
}
let data = ''
res.on('data', chunk => data += chunk)
res.on('end', () => parseYamlResponse(data, url, resolve, reject))
}).on('error', e => reject(new Error('Failed to fetch external ref ' + url + ': ' + e.message)))
})
}

function extractFragment(refValue) {
const hashIdx = refValue.indexOf('#')
return {
baseUrl: hashIdx >= 0 ? refValue.substring(0, hashIdx) : refValue,
fragment: hashIdx >= 0 ? refValue.substring(hashIdx + 1) : ''
}
}

function findObject(obj, propertiesArray) {
if (propertiesArray.length < 1) return obj
if (obj === undefined || obj === null) return undefined
const property = propertiesArray.shift()
return findObject(obj[property], propertiesArray)
}

function isExternalRef(refValue) {
return refValue.startsWith('http://') || refValue.startsWith('https://')
}

function resolveInternalRef(refValue, localDefinition) {
const fragment = refValue.replace(/^#\/?/, '')
const parts = fragment ? fragment.split('/') : []
const refObject = parts.length ? findObject(localDefinition, [...parts]) : localDefinition
if (refObject === undefined) { error('$ref not found: ' + refValue) }
return refObject
}

module.exports = function createRefsModule(opts) {
const prepareSiblings = opts?.prepareSiblings || function() { return {} }
const externalDocCache = new Map()

async function ensureFetched(baseUrl, seenUrls) {
if (externalDocCache.has(baseUrl)) return
if (seenUrls.has(baseUrl)) { error('Circular external $ref detected: ' + baseUrl) }
const childSeen = new Set(seenUrls)
childSeen.add(baseUrl)
let doc
try { doc = await fetchUrl(baseUrl) }
catch (e) { error(e.message) }
await resolveRefs(doc, doc, childSeen)
externalDocCache.set(baseUrl, doc)
}

async function fetchAndResolveExternal(refValue, seenUrls) {
const { baseUrl, fragment } = extractFragment(refValue)
await ensureFetched(baseUrl, seenUrls)
const doc = externalDocCache.get(baseUrl)
if (!fragment || fragment === '/') return doc
const parts = fragment.replace(/^\//, '').split('/')
const resolved = findObject(doc, parts)
if (resolved === undefined) { error('External $ref fragment not found: ' + fragment + ' in ' + baseUrl) }
return resolved
}

async function resolveRefs(obj, localDefinition, seenUrls = new Set()) {
for (const k in obj) {
if (typeof obj[k] === 'object' && obj[k] !== null) {
await resolveRefs(obj[k], localDefinition, seenUrls)
} else if (k === '$ref') {
const siblings = prepareSiblings(obj)
const refObject = isExternalRef(obj[k])
? await fetchAndResolveExternal(obj[k], seenUrls)
: resolveInternalRef(obj[k], localDefinition)
delete obj[k]
Object.assign(obj, refObject, siblings)
}
}
}

return resolveRefs
}
38 changes: 6 additions & 32 deletions src/parser/swagger2/refs.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,13 @@

'use strict'

function eachRecursive(obj) {
for (var k in obj) {
if (typeof obj[k] == "object" && obj[k] !== null) {
eachRecursive(obj[k]);
} else {
if(k == '$ref') {
let property = obj[k]
property = property.replace('#/', '')
let propertiesArray = property.split('/')
let refObject = findObject(global.definition, propertiesArray)
const createRefsModule = require('../refs')

// Clear ref property
delete obj[k]

// Assign properties refOcject
Object.assign(obj, refObject)
}
}
}
}

function findObject(obj, propertiesArray) {
if(propertiesArray.length < 1) {
return obj
}

let property = propertiesArray.shift()
return findObject(obj[property], propertiesArray)
}
const resolveRefs = createRefsModule()

module.exports = function() {
return function get() {
eachRecursive(global.definition)
return global.definition
return async function get() {
await resolveRefs(globalThis.definition, globalThis.definition)
return globalThis.definition
}
}()
}()
Loading