{"id":598,"date":"2017-02-01T17:08:57","date_gmt":"2017-02-01T16:08:57","guid":{"rendered":"https:\/\/solidt.eu\/site\/?p=598"},"modified":"2019-08-12T15:35:46","modified_gmt":"2019-08-12T14:35:46","slug":"node-express-proxy-request","status":"publish","type":"post","link":"https:\/\/solidt.eu\/site\/node-express-proxy-request\/","title":{"rendered":"Node: Express: proxy request"},"content":{"rendered":"\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">const express = require(\"express\");\nconst request = require(\"request\");\nconst app = express();\n\napp.use((req, res, next) => {\n    let data = '';\n    req.setEncoding('utf8');\n    req.on('data', (chunk) => data += chunk);\n    req.on('end', () => {\n        req.rawBody = data;\n        next();\n    });\n});\n\napp.get('\/', (_req, res) => res.send('Proxy requests via a POST call to \/ajax'));\n\nconst port = 4202;\napp.listen(port, () => console.log(`App listening on port ${port}!`))\napp.post('\/ajax', (req, res) => ajaxRequest(req, res));\n\nfunction formDataUrlEncoded(data) {\n    var str = [];\n    if (!data) return;\n    Object.keys(data).forEach(key =>str.push(encodeURIComponent(key) + \"=\" + encodeURIComponent(data[key])));\n    return str.join(\"&amp;\");\n}\nfunction formData(data) {\n    var str = [];\n    if (!data) return;\n    Object.keys(data).forEach(key => str.push(key + \"=\" + data[key]));\n    return str.join(\"&amp;\");\n}\n\nfunction ajaxRequest(req, res) {    \n    let settings = {};\n    if (req.rawBody) {\n        settings = JSON.parse(req.rawBody);\n    } else {\n        throw \"the body is empty.\";\n    }\n    return new Promise((resolve, reject) => {\n        if (!settings.url) throw \"the url-property must be specified.\";\n        settings.method = settings.method || \"GET\";\n        settings.headers = settings.headers || [];\n        settings.responseType = settings.responseType || \"text\";\n        settings.body = settings.body || settings.data || {};\n        settings.proxy = ''; \/\/ config.settings.proxyUrl\n        \/\/ Check valid data format\n        let isFormData = false;\n        let isFormDataUrlEncoded = false;\n        \/\/var isXml = false;\n        let isJson = false;\n        let contentTypeSet = false;\n        if (settings.contentType) {\n            contentTypeSet = true;\n            isFormData = settings.contentType.includes('form-data');\n            isFormDataUrlEncoded = settings.contentType.includes('form-urlencoded ');\n            \/\/isXml = settings.contentType.includes('xml');\n            isJson = settings.contentType.includes('json');\n        }\n        \/\/ Validate data and try to fix errors.\n        let postData = null;\n        if (settings.method.toLowerCase() !== 'get') {\n            if (isFormData) {\n                postData = formData(settings.data);\n            }\n            else if (isFormDataUrlEncoded) {\n                postData = formDataUrlEncoded(settings.data);\n            }\n            else if (isJson) {\n                postData = settings.data;\n                if ('string' !== typeof settings.data) {\n                    postData = JSON.stringify(settings.data);\n                }\n            }\n            else if (!contentTypeSet) {\n                \/\/ default to JSON\n                settings.contentType = 'application\/json';\n                \/\/ if (stringHelper.isValidJson(settings.data)) {\n                \/\/     postData = settings.data;\n                \/\/ } else if (stringHelper.canConvertToJsonString(settings.data)) {\n                \/\/     postData = JSON.stringify(settings.data);\n                \/\/ } else {\n                \/\/     throw \"mismatch between contenttype and data property.\";\n                \/\/ }\n            }\n        }\n        settings.body = postData;        \n        let result = request(settings, (error) => {\n            console.info(\"completed\", settings.url);\n            if (error) {\n                console.error(\"error_a\", error);\n                reject(new Error(error));\n            } else {\n                resolve();\n            }\n        });\n        result.on('response', response => {\n            result.pipe(res);\n            response.on('end', resolve);\n        });\n        result.on('error', err => {\n            console.error(\"error_b\", err);\n            reject(err);\n        });\n    })\n    .catch((err) => res.status(400).send(String(err)));\n}<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">var config = rfr(\"config\");\nrouter.route('\/data\/stream\/:requestUrl?')\n    .all((req, res) => {\n        var settings = {\n            method: req.method,\n            url: req.params.requestUrl,\n            headers: req.headers,\n            proxy: config.settings.proxyUrl || ''\n        };\n        var webRequest = request(settings);\n        req.pipe(webRequest);\n        webRequest.pipe(res);\n        var headersSend = false;\n        webRequest.on('data', function() {\n            if (!headersSend) {\n                headersSend = true;\n                try {\n                    res.writeHead(webRequest.response.statusCode, webRequest.response.headers);\n                } catch (e) {\n                    \/\/ can not write headers, ignore!\n                    log.info(\"can not write headers when they are already send\");\n                }\n            }\n        });\n        \/\/ webRequest.on('end', function (response) {\n        \/\/ });\n    });\n<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">service.proxyURL = function(url) {\n    let proxyUrl = window.location.origin + '\/api\/data\/stream\/';\n    if (proxyUrl.indexOf(':\/\/') > -1) {\n\n        if (url.indexOf(proxyUrl) !== -1) {\n            \/\/ url already is a NAPI streaming url\n            return url;\n        }\n\n        let baseUrl = '';\n        if (url.indexOf(':\/\/') !== -1) {\n            \/\/ url is full url\n        } else if (url[0] === '\/') {\n            \/\/ url is relative to root\n            baseUrl = window.location.origin;\n        } else {\n            baseUrl = window.location.href + '\/';\n            \/\/ url is relative to current url\n        }\n        url = baseUrl + url;\n\n        let encodedUrl = encodeURIComponent(url);\n        return proxyUrl + encodedUrl;\n    }\n    return url;\n};<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">var xhrService = {};\nvar Promise = require(\"bluebird\");\nvar rfr = require(\"rfr\");\nvar log = rfr(\"services\/logService\").getLogger();\nvar request = require(\"request\");\nvar config = rfr(\"services\/configService\").getConfig();\nvar stringHelper = rfr(\"helpers\/stringHelper\");\n\n\/\/ global\nvar XMLHttpRequest = require('xhr2');\n\n\nxhrService.xhr = function () {\n    return new XMLHttpRequest();\n};\n\nvar formDataUrlEncoded = function (data) {\n    var str = [];\n    if (data) {\n        Object.keys(data).forEach(function (key) {\n            str.push(encodeURIComponent(key) + \"=\" + encodeURIComponent(data[key]));\n        });\n    }\n    return str.join(\"&amp;\");\n};\n\nvar formData = function (data) {\n    var str = [];\n    if (data) {\n        Object.keys(data).forEach(function (key) {\n            str.push(key + \"=\" + data[key]);\n        });\n    }\n    return str.join(\"&amp;\");\n};\n\nvar authErrorHandler = null;\nvar onAuthError = function (xhr, event) {\n    if (authErrorHandler) { authErrorHandler(xhr, event); }\n};\n\nxhrService.setAuthErrorHandler = function (handler) {\n    authErrorHandler = handler;\n};\n\n\nvar timeoutHandler = null;\nvar onTimeout = function (xhr, event) {\n    if (timeoutHandler) { timeoutHandler(xhr, event); }\n};\n\nxhrService.setTimeoutHandler = function (handler) {\n    timeoutHandler = handler;\n};\n\nvar networkErrorHandler = null;\nvar onNetworkError = function (xhr, event) {\n    if (networkErrorHandler) { networkErrorHandler(xhr, event); }\n};\n\nxhrService.setNetworkErrorHandler = function (handler) {\n    networkErrorHandler = handler;\n};\n\nvar networkSuccessHandler = null;\nvar onNetworkSuccess = function (xhr, event) {\n    if (networkSuccessHandler) { networkSuccessHandler(xhr, event); }\n};\n\nxhrService.setNetworkSuccessHandler = function (handler) {\n    networkSuccessHandler = handler;\n};\n\nxhrService.request = function (settings) {\n    settings = settings || {};\n\n    return new Promise(function (resolve, reject) {\n\n\n        var method = (settings.method || \"GET\").toUpperCase();\n        var url = settings.url || \"\";\n        var data = settings.data;\n        var contentType = settings.contentType || \"x-www-form-urlencoded\";\n        var headers = settings.headers || [];\n        var responseType = settings.responseType || \"text\";\n        var postData = null;\n\n        if (method === \"POST\" || method === \"PUT\")\n        {\n            if (contentType === \"x-www-form-urlencoded\") {\n                postData = formDataUrlEncoded(data);\n            } else if (typeof data !== \"string\") {\n                postData = JSON.stringify(data);\n            } else {\n                postData = data;\n            }\n        } else {\n            url += \"?\" + formDataUrlEncoded(data);\n        }\n\n        var xhr = xhrService.xhr();\n        if (!xhr) {\n\n            reject(\"Error: could not create a XMLHttpRequest\");\n            return;\n        }\n\n        xhr.open(method, url, true);\n\n        if (headers) {\n            Object.keys(headers).forEach(function (key) {\n                xhr.setRequestHeader(key, headers[key]);\n            });\n        }\n\n        \/\/xhr.timeout = 30000; \/\/ 30 seconds\n\n        xhr.addEventListener(\"error\", function (event) {\n            onNetworkError(xhr, event);\n\n            reject(\"Error: network error\");\n        });\n        xhr.addEventListener(\"timeout\", function (event) {\n            onTimeout(xhr, event);\n\n            reject(\"Error: timeout error\");\n        });\n\n        xhr.responseType = responseType;\n        xhr.addEventListener(\"load\", function (event) {\n            log.info(\"Request completed\", url, xhr.status);\n\n            if (xhr.status != 200 &amp;&amp; xhr.status != 201 &amp;&amp; xhr.status != 304) {\n                reject(xhr);\n            }\n            else {\n                \/\/onNetworkSuccess(xhr, event);\n\n                resolve(xhr, xhr.response);\n            }\n        }, false);\n\n        \/\/ log.info({\n        \/\/     method: method,\n        \/\/     url: url,\n        \/\/     headers: headers,\n        \/\/     data: postdata\n        \/\/ });\n\n        xhr.send(postData);\n    });\n};\n\nxhrService.requestProxyStream = function (req, resp, settings) {\n    return new Promise(function (resolve, reject) {\n        settings.proxy = config.settings.proxyUrl || '';\n        var result = request(settings);\n        req.pipe(result);\n        result.pipe(resp);\n        result.on('response', function (response) {\n            response.on('end', resolve);\n        });\n        result.on('error', reject);\n    });\n};\n\nxhrService.requestProxy = function (settings) {\n    settings = settings || {};\n\n    return new Promise(function (resolve, reject) {\n\n        \/*\n        *  In contrast to the xhrService.request function\n        *  this function doesn't use the xhr2 package.\n        *  This function uses the request package instead.\n        *\n        *  The request object does not have te\n        *  properties \"response\" or \"responseText\".\n        *  Instead it has a property namend \"body\".\n        *\n        *  Also when sending data there is nog \"data\" property\n        *  on the settingsobject. Instad a \"body\" property is used.\n        *\n        *  The property contains the UNCOMPRESSED data for the request.\n        *  To keep the data valid I used the onData event to return the\n        *  COMPRESSED data instead.\n        *\n        *\/\n\n        \/\/ Make sure some properties are set or set default values.\n        if (!settings.url) throw 'the url-property must be spedificed. ';\n        settings.method = settings.method || \"GET\";\n        settings.headers = settings.headers || [];\n        settings.responseType = settings.responseType || \"text\";\n        settings.body = settings.body || settings.data || {};\n        settings.proxy = config.settings.proxyUrl || '';\n\n        \/\/ Check valid data format\n        var isFormData = false;\n        var isFormDataUrlEncoded = false;\n        \/\/var isXml = false;\n        var isJson = false;\n        var contentTypeSet = false;\n\n        if (settings.contentType) {\n            contentTypeSet = true;\n            isFormData = -1 !== settings.contentType.indexOf('form-data');\n            isFormDataUrlEncoded = -1 !== settings.contentType.indexOf('form-urlencoded ');\n            \/\/isXml = -1 !== settings.contentType.indexOf('xml');\n            isJson = -1 !== settings.contentType.indexOf('json');\n        }\n\n        \/\/ Validate data and try to fix errors.\n        var postData = null;\n        if (settings.method.toLowerCase() !== 'get') {\n            if (isFormData) {\n                postData = formData(settings.data);\n            }\n            else if (isFormDataUrlEncoded) {\n                postData = formDataUrlEncoded(settings.data);\n            }\n            \/\/ else if (isXml) {\n            \/\/\n            \/\/ }\n            else if (isJson) {\n                postData = settings.data;\n                if ('string' !== typeof settings.data) {\n                    postData = JSON.stringify(settings.data);\n                }\n            }\n            else if (!contentTypeSet) {\n                \/\/ default to JSON\n                settings.contentType = 'application\/json';\n                if (stringHelper.isValidJson(settings.data)) {\n                    postData = settings.data;\n                }\n                else if (stringHelper.canConvertToJsonString(settings.data)) {\n                    postData = JSON.stringify(settings.data);\n                }\n                else {\n                    throw 'mismatch between contenttype and data property.';\n                }\n            }\n        }\n        settings.body = postData;\n        \n        \/\/ Do the request\n        request(settings, function (error, response, body) {\n            log.info(\"RequestProxy completed\", settings.url);\n            \/\/ response and body are unused\n            if (error) {\n                reject(new Error(error));\n            }\n        })\n        .on('response', function (response) {\n\n            \/\/ Unmodified http.IncomingMessage object\n            response.on('data', function (data) {\n                \/\/ Compressed data as it is received\n                if (typeof response.rawData == 'undefined') {\n                    response.rawData = data; \/\/ create new property\n                }\n                else {\n                    if (data instanceof Buffer) {\n                        response.rawData = Buffer.concat([response.rawData, data]);\n                    }\n                    else {\n                        response.rawData += data;\n                    }\n\n                }\n            })\n            .on('end', function () {\n                resolve(response);\n            });\n\n        });\/\/.pipe(fs.createWriteStream('doodle.png'));\n\n\n    });\n};\n\nxhrService.requestProxy2 = function (req, res, settings) {\n    settings = settings || {};\n\n    return new Promise(function (resolve, reject) {\n\n        \/*\n        *  In contrast to the xhrService.request function\n        *  this function doesn't use the xhr2 package.\n        *  This function uses the request package instead.\n        *\n        *  The request object does not have te\n        *  properties \"response\" or \"responseText\".\n        *  Instead it has a property namend \"body\".\n        *\n        *  Also when sending data there is nog \"data\" property\n        *  on the settingsobject. Instad a \"body\" property is used.\n        *\n        *  The property contains the UNCOMPRESSED data for the request.\n        *  To keep the data valid I used the onData event to return the\n        *  COMPRESSED data instead.\n        *\n        *\/\n\n        \/\/ Make sure some properties are set or set default values.\n        if (!settings.url) throw 'the url-property must be spedificed. ';\n        settings.method = settings.method || \"GET\";\n        settings.headers = settings.headers || [];\n        settings.responseType = settings.responseType || \"text\";\n        settings.body = settings.body || settings.data || {};\n        settings.proxy = config.settings.proxyUrl || '';\n\n        \/\/ Check valid data format\n        var isFormData = false;\n        var isFormDataUrlEncoded = false;\n        \/\/var isXml = false;\n        var isJson = false;\n        var contentTypeSet = false;\n\n        if (settings.contentType) {\n            contentTypeSet = true;\n            isFormData = -1 !== settings.contentType.indexOf('form-data');\n            isFormDataUrlEncoded = -1 !== settings.contentType.indexOf('form-urlencoded ');\n            \/\/isXml = -1 !== settings.contentType.indexOf('xml');\n            isJson = -1 !== settings.contentType.indexOf('json');\n        }\n\n        \/\/ Validate data and try to fix errors.\n        var postData = null;\n        if (settings.method.toLowerCase() !== 'get') {\n            if (isFormData) {\n                postData = formData(settings.data);\n            }\n            else if (isFormDataUrlEncoded) {\n                postData = formDataUrlEncoded(settings.data);\n            }\n            \/\/ else if (isXml) {\n            \/\/\n            \/\/ }\n            else if (isJson) {\n                postData = settings.data;\n                if ('string' !== typeof settings.data) {\n                    postData = JSON.stringify(settings.data);\n                }\n            }\n            else if (!contentTypeSet) {\n                \/\/ default to JSON\n                settings.contentType = 'application\/json';\n                if (stringHelper.isValidJson(settings.data)) {\n                    postData = settings.data;\n                }\n                else if (stringHelper.canConvertToJsonString(settings.data)) {\n                    postData = JSON.stringify(settings.data);\n                }\n                else {\n                    throw 'mismatch between contenttype and data property.';\n                }\n            }\n        }\n        settings.body = postData;\n        \n        \/\/ Do the request\n        var result = request(settings, function (error, response, body) {\n            log.info(\"RequestProxy_1 completed\", settings.url);\n            \/\/ response and body are unused\n            if (error) {\n                log.error(\"RequestProxy_1 error_1\", error);\n                reject(new Error(error));\n            }\n        });\n\n        var myTimeout = setTimeout(function() {\n            \/\/res.status(500).send({ error: 'something blew up' });\n            result = request(settings, function (error, response, body) {\n                log.error(\"RequestProxy_2 completed\", settings.url);\n                \/\/ response and body are unused\n                if (error) {\n                    log.error(\"RequestProxy_1 error_2\", error);\n                    reject(new Error(error));\n                }\n            });\n\n            result.on('response', function (response) {\n                result.pipe(res);\n                response.on('end', resolve);\n            });\n            result.on('error', function(err) {\n                log.error(\"RequestProxy_2 error_2\", err);\n                reject(err);\n            });\n        }, 3000);\n\n        result.on('response', function (response) {\n            clearTimeout(myTimeout);\n            result.pipe(res);\n            response.on('end', resolve);\n        });\n        result.on('error', function(err) {\n            log.error(\"RequestProxy_1 error_2\", err);\n            reject(err);\n        });\n    });\n};\n\n\nmodule.exports = xhrService;\n<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">\/\/ Packages to use\nvar Promise = require(\"bluebird\");\nvar rfr = require(\"rfr\");\nvar log = rfr(\"services\/logService\").getLogger();\nvar xhrService = rfr(\"services\/xhrService\");\n\nvar dataService = {};\n\ndataService.delegateRequest = function (req, res) {\n\n    var requestType = req.params.requestType;\n    var asyncResult = null;\/\/Promise.reject('invalid type: ' + requestType);\n    switch (requestType) {\n        case 'ajax':\n            var settings = {};\n            if (req.rawBody) {\n                settings = JSON.parse(req.rawBody);\n            }\n            asyncResult = dataService.ajax(req, res, settings);\n            break;\n        case 'url':\n            asyncResult = dataService.url(req);\n            break;\n        case 'stream':\n            asyncResult = dataService.stream(req, res);\n            break;\n        case 'local':\n            asyncResult = dataService.local(req);\n            break;\n        default:\n            \/\/default code block\n            asyncResult = Promise.reject('invalid type: ' + requestType);\n            log.info(\"delegateRequest(): request type unknown \", requestType);\n    }\n    return asyncResult;\n};\n\ndataService.ajax = (req, res, settings) => {\n    return new Promise(function (resolve, reject) {\n        \/\/ all ajax request use the proxy if proxyUrl is set in the config;\n        \/*\n        var request = xhrService.requestProxy(settings);        \n        request.then((xhr) => {\n            var result = {\n                xhr: xhr,\n                contentBuffer: ''\n            };\n\n            if (xhr.rawData &amp;&amp; xhr.rawData instanceof Buffer) {\n                result.contentBuffer = xhr.rawData;\n            } else {\n                var arrayBuffer = xhr.body;\n                var byteArray = new Uint8Array(arrayBuffer);\n                result.contentBuffer = new Buffer(byteArray, 'binary');\n            }\n            resolve(result);\n        })\n        .catch((xhr) => {\n            log.error(xhr);\n            reject(xhr);\n        });\n        *\/\n        return xhrService.requestProxy2(req, res, settings);\n    });\n};\n\ndataService.url = (request) => {\n    return new Promise(function (resolve, reject) {\n        var settings = setupSettingsObjectFromRequest(request);\n        dataService.ajax(settings)\n            .then((result) => {\n                resolve(result);\n            })\n            .catch((xhr) => {\n                reject(xhr);\n            });\n    });\n};\n\ndataService.stream = (req, resp) => {\n    return new Promise(function (resolve, reject) {\n        var settings = setupSettingsObjectFromRequest(req);\n        return xhrService.requestProxyStream(req, resp, settings);\n    });\n};\n\ndataService.local = (settings) => {\n    return new Promise(function (resolve, reject) {\n        reject('not implemented yet');\n    });\n};\n\nmodule.exports = dataService;\n\n\/* Define some helper functions for this service *\/\nfunction setupSettingsObjectFromRequest(request) {\n    \/\/ setup settings from current request\n    var settings = {};\n    settings.headers = request.headers || {};\n\n    var url = request.params.requestUrl || throwError('requestUrlNotSet');\n    settings.headers.host = url;\n    settings.method = request.method;\n    settings.url = url;\n    settings.responseType = \"arraybuffer\";\n\n    if (request.rawBody) {\n        var body = JSON.parse(request.rawBody) || {};\n        settings.data = body;\n    }\n\n    \/\/ add some default properties\n    settings.crossDomain = true;\n    settings.headers['cache-control'] = 'no-cache';\n\n    return settings;\n}\n\nfunction throwError(errorMessage) {\n    throw new Error(errorMessage);\n}\n<\/pre>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[5],"tags":[],"class_list":["post-598","post","type-post","status-publish","format-standard","hentry","category-javascript"],"_links":{"self":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/598","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/comments?post=598"}],"version-history":[{"count":9,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/598\/revisions"}],"predecessor-version":[{"id":2486,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/598\/revisions\/2486"}],"wp:attachment":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/media?parent=598"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/categories?post=598"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/tags?post=598"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}