Cannot install ACE React app from Heroku

Hi. I´m creating a marketplace app running on Jira using Atlassian Connect Next It runs fine locally and builds also fine on Heroku.

The problem is when I try to install the app to Jira through the Manage Apps page. I put in the https://bcj.herokuapp.com/atlassian-connect.json link and the install process starts. Somewhere in the process Jira sends a POST call to /installed and gets no answer. The error in Jira says:

The app host did not respond when we tried to contact it at "https://bcj.herokuapp.com/installed" during installation (the attempt timed out). Please try again later or contact the app vendor.

In Heroku the following error is logged:

2021-06-22T11:03:42.917720+00:00 heroku[router]: at=error code=H12 desc="Request timeout" method=POST path="/installed" host=bcj.herokuapp.com request_id=65e25ca8-c210-4f2d-a5bf-f4384fc3627f fwd="52.215.192.228" dyno=web.1 connect=1ms service=30000ms status=503 bytes=0 protocol=https
2021-06-22T11:03:42.921153+00:00 app[web.1]: ::ffff:10.35.52.190 - - [22/Jun/2021:11:03:42 +0000] "POST /installed HTTP/1.1" - - "-" "Atlassian HttpClient unknown / JIRA-1001.0.0-SNAPSHOT (100166) / Atlassian-Connect/1001.0.0-SNAPSHOT"

Another thing that I noticed trying to debug this is that the postgres db in Heroku does not get anything. No table gets created or anything.

config.json

// It's OK to write comments in this JSON configuration file,
// comments will be stripped when this file is loaded.
{
    // "development" is the default environment.
    // To change, set NODE_ENV (http://expressjs.com/api.html#app.configure).
    "development": {
        // Port the Express server will listen on.
        "port": 3000,
        // Use views/unauthorized.hbs for error page.
        "errorTemplate": true,
        // atlassian-connect-express currently integrates with Sequelize for
        // persistence to store the host client information (i.e. client key,
        // host public key etc.). When no adapter is specified, it defaults to
        // memory.
        //
        // To specify a storage other than memory, set
        // "dialect" to one of Sequelize's other supported dialects:
        // http://docs.sequelizejs.com/manual/installation/usage.html#dialects
        //
        // To use a custom storage adapter, configure it in "store".
        // Make sure to register the adapter factory in app.js:
        //
        //   ace.store.register(adapterName, factoryFunction)
        //
        // At https://bitbucket.org/atlassian/atlassian-connect-express/src/master/lib/store/,
        // see index.js and sequelize.js for code demonstrating how to write
        // a conformant adapter. The default values are as follows:
        //
        //   "store": {
        //     "adapter": "sequelize",
        //     "dialect": "sqlite3",
        //     "storage": ":memory:"
        //   },
        //
        // To configure PostgreSQL, the following can be used:
        //
        //   "store": {
        //     "adapter": "sequelize",
        //     "dialect": "postgres",
        //     "url": "postgres://localhost/my_app_database"
        //   },
        //
        // An appropriate DB driver for Sequelize is required for storage other than memory.
        // For PostgreSQL, run the following command:
        //
        //   npm install --save pg
        "store": {
            "adapter": "sequelize",
            "dialect": "sqlite3",
            "type": "memory"
        }
    },
    // Configuration for production, which is enabled by setting
    // the NODE_ENV=production environment variable.
    "production": {
        // PaaS like Heroku will provide HTTP port via environement variable.
        "port": "$PORT",
        // Use views/unauthorized.hbs for error page.
        "errorTemplate": true,
        // Public URL to production app.
        "localBaseUrl": "https://bcj.herokuapp.com",
        "store": {
            // Don't use memory storage for production, otherwise
            // data in the storage will go away when the app server restarts.
            // Here, we use PostgreSQL:
            "type": "postgres",
            // PaaS like Heroku will provide DB connection URL through environment variable.
            "url": "$DATABASE_URL"
            // Bætti við í samræmi við tillögu af vef: https://stackoverflow.com/questions/25000183/node-js-postgresql-error-no-pg-hba-conf-entry-for-host/64960461#64960461
            // "dialectOptions": {
            //     "ssl": {"require":true,
            //             "rejectUnauthorized": false
            //         }
            //     }
        },
        "maxTokenAge": 7890000,
        // The app can only be registered by the products on these domains:
        "whitelist": [
            "*.jira-dev.com",
            "*.atlassian.net",
            "*.atlassian.com",
            "*.jira.com",
            "*.localhost:3000.com"
        ]
    },
    "product": "jira"
}

atlassian-connect.json

{
    "key": "bug-consolidator-dev-vala",
    "name": "Bug consolidator-dev-vala",
    "description": "My Next.js Atlassian App",
    "enableLicensing": true,
    "baseUrl": "{{localBaseUrl}}",
    "links": {
        "self": "{{localBaseUrl}}/atlassian-connect.json"
    },
    "authentication": {
        "type": "jwt"
    },
    "lifecycle": {
        "installed": "/installed"
    },
    "scopes": [
        "READ",
        "WRITE"
    ],
    "modules": {
        "generalPages": [
            {
                "key": "landing",
                "location": "system.top.navigation.bar",
                "name": {
                    "value": "dev-vala"
                },
                "url": "/landing",
                "conditions": [
                    {
                        "condition": "user_is_logged_in"
                    }
                ]
            },
            {
                "key": "upload",
                "location": "none",
                "name": {
                    "value": "Upload UP collection"
                },
                "url": "/upload",
                "conditions": [
                    {
                        "condition": "user_is_logged_in"
                    }
                ]
            }
        ]
    }
}

Thanks in advance for the help.

@HallbjrnMagnsson from the top of my head consider looking into this first:

  • which Node/Express/ACE version you’re using?
  • do you have a route /installed custom coded in you script?
    • if yes, please share the contents.
    • if no, this is good - ACE has this route auto-mapped when initializing add-on on application start.
      • You could also add route list printing to your code when in dev mode (see discussion)
  • try sending POST to /installed to confirm it returns 201 or 204 HTTP codes (per AC specs)
  • check your network routing and firewall on Heroku - whether external (incoming) requests are accepted (e.g. your host might be set up to allow access from local IPs only)
  • check if you have PORT environment variable set and if it contains proper value. Try hard-coding the value first to test if it works and then move to environment variable.

As for PostgreSQL, check if you have an environment variable $DATABASE_URL set to some value and whether this value is correct (i.e. actually is proper location for PostgreSQL DB and includes authentication details (if it is set up to require auth)). Try hard-coding the value first to test if it works and then move to environment variable.

Hope above makes sense and get you going.

1 Like

Hi @ViliusZigmantas and @ibuchanan I want to thank you both for helping me out.

To answer your questions @ViliusZigmantas first:

  • I was running an old version of ACE but I have now upgraded to the latest with no change

  • There is no POST route custom code the only custom route codes are GET.

  • Sending POST to /installed using Postman gives the same result (503)

  • External routing is open since I can GET /atlassian-connect.json

  • Heroku sets the PORT environmental variability automatically and I can verify it.

  • When installing the Postgres service in Heroku, Heroku sets a $DATABASE_URL with correct credentials which I have verified.

Thank you as well for the suggestions of using Atlassian Connect Inspector, I knew of it I just somehow had not connected the dots on the usefulness of it.

@ibuchanan Thank you for the suggestion of trying to log/trap the transaction. I have tried a lot to get it to work. The best I got was by creating a server.post route in routes.js that logged out the call. By doing so I however broke the app as this interfered apparently with the ACE routing. Following is the body of the req that was logged:

body: {
        key: 'bug-consolidator-for-jira-heroku',
        clientKey: '4c72d0bb-d200-332f-8d42-6e7b66f88ccb',
        publicKey: 'MIGf... "Center part removed for privacy" ...AQAB',
        sharedSecret: 'L1BD... "Center part removed for privacy" ...OLzA',
        serverVersion: '100167',
        pluginsVersion: '1001.0.0-SNAPSHOT',
        baseUrl: 'https://hiproject.atlassian.net',
        productType: 'jira',
        description: 'Atlassian JIRA at https://hiproject.atlassian.net ',
        eventType: 'installed'
    },

I see nothing wrong with it. it´s exactly the same as is logged when I install the app in dev mode. See below:

body: {
    key: 'bug-consolidator-for-jira',
    clientKey: '4c72... "Center part removed for privacy" ...8ccb',
    publicKey: 'MIGf... "Center part removed for privacy" ...AQAB',
    sharedSecret: 'pmI1... "Center part removed for privacy" ...k2Pg',
    serverVersion: '100166',
    pluginsVersion: '1001.0.0-SNAPSHOT',
    baseUrl: 'https://hiproject.atlassian.net',
    productType: 'jira',
    description: 'Atlassian JIRA at https://hiproject.atlassian.net ',
    eventType: 'installed'
}

Following is the full req that I logged.

{
    _readableState: ReadableState {
        objectMode: false,
        highWaterMark: 16384,
        buffer: BufferList { head: null, tail: null, length: 0 },
        length: 0,
        pipes: [],
        flowing: true,
        ended: true,
        endEmitted: true,
        reading: false,
        sync: false,
        needReadable: false,
        emittedReadable: false,
        readableListening: false,
        resumeScheduled: false,
        errorEmitted: false,
        emitClose: true,
        autoDestroy: false,
        destroyed: false,
        errored: null,
        closed: false,
        closeEmitted: false,
        defaultEncoding: 'utf8',
        awaitDrainWriters: null,
        multiAwaitDrain: false,
        readingMore: false,
        decoder: null,
        encoding: null,
        [Symbol(kPaused)]: false
    },
    _events: [Object: null prototype] { end: [Function: clearRequestTimeout] },
        _eventsCount: 1,
        _maxListeners: undefined,
        socket: <ref *1> Socket {
        connecting: false,
        _hadError: false,
        _parent: null,
        _host: null,
        _readableState: ReadableState {
            objectMode: false,
            highWaterMark: 16384,
            buffer: BufferList { head: null, tail: null, length: 0 },
            length: 0,
            pipes: [],
            flowing: true,
            ended: false,
            endEmitted: false,
            reading: true,
            sync: false,
            needReadable: true,
            emittedReadable: false,
            readableListening: false,
            resumeScheduled: false,
            errorEmitted: false,
            emitClose: false,
            autoDestroy: false,
            destroyed: false,
            errored: null,
            closed: false,
            closeEmitted: false,
            defaultEncoding: 'utf8',
            awaitDrainWriters: null,
            multiAwaitDrain: false,
            readingMore: false,
            decoder: null,
            encoding: null,
            [Symbol(kPaused)]: false
        },
        _events: [Object: null prototype] {
            end: [Array],
            timeout: [Function: socketOnTimeout],
            data: [Function: bound socketOnData],
            error: [Array],
            close: [Array],
            drain: [Function: bound socketOnDrain],
            resume: [Function: onSocketResume],
            pause: [Function: onSocketPause]
        },
        _eventsCount: 8,
        _maxListeners: undefined,
        _writableState: WritableState {
            objectMode: false,
            highWaterMark: 16384,
            finalCalled: false,
            needDrain: false,
            ending: false,
            ended: false,
            finished: false,
            destroyed: false,
            decodeStrings: false,
            defaultEncoding: 'utf8',
            length: 0,
            writing: false,
            corked: 0,
            sync: true,
            bufferProcessing: false,
            onwrite: [Function: bound onwrite],
            writecb: null,
            writelen: 0,
            afterWriteTickInfo: null,
            buffered: [],
            bufferedIndex: 0,
            allBuffers: true,
            allNoop: true,
            pendingcb: 0,
            prefinished: false,
            errorEmitted: false,
            emitClose: false,
            autoDestroy: false,
            errored: null,
            closed: false,
            closeEmitted: false
            },
        allowHalfOpen: true,
        _sockname: null,
        _pendingData: null,
        _pendingEncoding: '',
        server: Server {
            maxHeaderSize: undefined,
            insecureHTTPParser: undefined,
            _events: [Object: null prototype],
            _eventsCount: 2,
            _maxListeners: undefined,
            _connections: 1,
            _handle: [TCP],
            _usingWorkers: false,
            _workers: [],
            _unref: false,
            allowHalfOpen: true,
            pauseOnConnect: false,
            httpAllowHalfOpen: false,
            timeout: 0,
            keepAliveTimeout: 5000,
            maxHeadersCount: null,
            headersTimeout: 60000,
            requestTimeout: 0,
            _connectionKey: '6::::16988',
            [Symbol(IncomingMessage)]: [Function: IncomingMessage],
            [Symbol(ServerResponse)]: [Function: ServerResponse],
            [Symbol(kCapture)]: false,
            [Symbol(async_id_symbol)]: 9
        },
        _server: Server {
            maxHeaderSize: undefined,
            insecureHTTPParser: undefined,
            _events: [Object: null prototype],
            _eventsCount: 2,
            _maxListeners: undefined,
            _connections: 1,
            _handle: [TCP],
            _usingWorkers: false,
            _workers: [],
            _unref: false,
            allowHalfOpen: true,
            pauseOnConnect: false,
            httpAllowHalfOpen: false,
            timeout: 0,
            keepAliveTimeout: 5000,
            maxHeadersCount: null,
            headersTimeout: 60000,
            requestTimeout: 0,
            _connectionKey: '6::::16988',
            [Symbol(IncomingMessage)]: [Function: IncomingMessage],
            [Symbol(ServerResponse)]: [Function: ServerResponse],
            [Symbol(kCapture)]: false,
            [Symbol(async_id_symbol)]: 9
        },
        parser: HTTPParser {
            '0': [Function: bound setRequestTimeout],
            '1': [Function: parserOnHeaders],
            '2': [Function: parserOnHeadersComplete],
            '3': [Function: parserOnBody],
            '4': [Function: parserOnMessageComplete],
            '5': [Function: bound onParserExecute],
            '6': [Function: bound onParserTimeout],
            _headers: [],
            _url: '',
            socket: [Circular *1],
            incoming: [Circular *2],
            outgoing: null,
            maxHeaderPairs: 2000,
            _consumed: true,
            onIncoming: [Function: bound parserOnIncoming],
            [Symbol(resource_symbol)]: [HTTPServerAsyncResource]
        },
        on: [Function: socketListenerWrap],
        addListener: [Function: socketListenerWrap],
        prependListener: [Function: socketListenerWrap],
        _paused: false,
        _httpMessage: ServerResponse {
            _events: [Object: null prototype],
            _eventsCount: 2,
            _maxListeners: undefined,
            outputData: [],
            outputSize: 0,
            writable: true,
            destroyed: false,
            _last: false,
            chunkedEncoding: false,
            shouldKeepAlive: false,
            _defaultKeepAlive: true,
            useChunkedEncodingByDefault: true,
            sendDate: true,
            _removedConnection: false,
            _removedContLen: false,
            _removedTE: false,
            _contentLength: null,
            _hasBody: true,
            _trailer: '',
            finished: false,
            _headerSent: false,
            socket: [Circular *1],
            _header: null,
            _keepAliveTimeout: 5000,
            _onPendingData: [Function: bound updateOutgoingData],
            _sent100: false,
            _expect_continue: false,
            req: [Circular *2],
            locals: [Object],
            _startAt: undefined,
            _startTime: undefined,
            writeHead: [Function: writeHead],
            __onFinished: [Function],
            [Symbol(kCapture)]: false,
            [Symbol(kNeedDrain)]: false,
            [Symbol(corked)]: 0,
            [Symbol(kOutHeaders)]: [Object: null prototype]
        },
        _peername: { address: '::ffff:10.13.124.73', family: 'IPv6', port: 22969 },
        [Symbol(async_id_symbol)]: 51,
        [Symbol(kHandle)]: TCP {
            reading: true,
            onconnection: null,
            _consumed: true,
            [Symbol(owner_symbol)]: [Circular *1]
        },
        [Symbol(kSetNoDelay)]: false,
        [Symbol(lastWriteQueueSize)]: 0,
        [Symbol(timeout)]: null,
        [Symbol(kBuffer)]: null,
        [Symbol(kBufferCb)]: null,
        [Symbol(kBufferGen)]: null,
        [Symbol(kCapture)]: false,
        [Symbol(kBytesRead)]: 0,
        [Symbol(kBytesWritten)]: 0,
        [Symbol(RequestTimeout)]: undefined
    },
    httpVersionMajor: 1,
    httpVersionMinor: 1,
    httpVersion: '1.1',
    complete: true,
    headers: {
        host: 'bcj.herokuapp.com',
        connection: 'close',
        authorization: 'JWT eyJ0... "Center part removed for privacy" ...PooU',
        accept: '*/*',
        'atlassian-connect-version': '1001.0.0-SNAPSHOT',
        'content-type': 'application/json; charset=UTF-8',
        'user-agent': 'Atlassian HttpClient unknown / JIRA-1001.0.0-SNAPSHOT (100167) / Atlassian-Connect/1001.0.0-SNAPSHOT',
        'x-request-id': 'ca081213-9ca0-4b24-bdba-40d18d6da334',
        'x-forwarded-for': '52.215.192.232',
        'x-forwarded-proto': 'https',
        'x-forwarded-port': '443',
        via: '1.1 vegur',
        'connect-time': '0',
        'x-request-start': '1625157883730',
        'total-route-time': '0',
        'content-length': '646'
    },
    rawHeaders: [
        'Host',
        'bcj.herokuapp.com',
        'Connection',
        'close',
        'Authorization',
        'JWT eyJ0... "Center part removed for privacy" ...PooU',
        'Accept',
        '*/*',
        'Atlassian-Connect-Version',
        '1001.0.0-SNAPSHOT',
        'Content-Type',
        'application/json; charset=UTF-8',
        'User-Agent',
        'Atlassian HttpClient unknown / JIRA-1001.0.0-SNAPSHOT (100167) / Atlassian-Connect/1001.0.0-SNAPSHOT',
        'X-Request-Id',
        'ca081213-9ca0-4b24-bdba-40d18d6da334',
        'X-Forwarded-For',
        '52.215.192.232',
        'X-Forwarded-Proto',
        'https',
        'X-Forwarded-Port',
        '443',
        'Via',
        '1.1 vegur',
        'Connect-Time',
        '0',
        'X-Request-Start',
        '1625157883730',
        'Total-Route-Time',
        '0',
        'Content-Length',
        '646'
    ],
    trailers: {},
    rawTrailers: [],
    aborted: false,
    upgrade: false,
    url: '/installed',
    method: 'POST',
    statusCode: null,
    statusMessage: null,
    client: <ref *1> Socket {
        connecting: false,
        _hadError: false,
        _parent: null,
        _host: null,
        _readableState: ReadableState {
            objectMode: false,
            highWaterMark: 16384,
            buffer: BufferList { head: null, tail: null, length: 0 },
            length: 0,
            pipes: [],
            flowing: true,
            ended: false,
            endEmitted: false,
            reading: true,
            sync: false,
            needReadable: true,
            emittedReadable: false,
            readableListening: false,
            resumeScheduled: false,
            errorEmitted: false,
            emitClose: false,
            autoDestroy: false,
            destroyed: false,
            errored: null,
            closed: false,
            closeEmitted: false,
            defaultEncoding: 'utf8',
            awaitDrainWriters: null,
            multiAwaitDrain: false,
            readingMore: false,
            decoder: null,
            encoding: null,
            [Symbol(kPaused)]: false
        },
        _events: [Object: null prototype] {
            end: [Array],
            timeout: [Function: socketOnTimeout],
            data: [Function: bound socketOnData],
            error: [Array],
            close: [Array],
            drain: [Function: bound socketOnDrain],
            resume: [Function: onSocketResume],
            pause: [Function: onSocketPause]
        },
        _eventsCount: 8,
        _maxListeners: undefined,
        _writableState: WritableState {
            objectMode: false,
            highWaterMark: 16384,
            finalCalled: false,
            needDrain: false,
            ending: false,
            ended: false,
            finished: false,
            destroyed: false,
            decodeStrings: false,
            defaultEncoding: 'utf8',
            length: 0,
            writing: false,
            corked: 0,
            sync: true,
            bufferProcessing: false,
            onwrite: [Function: bound onwrite],
            writecb: null,
            writelen: 0,
            afterWriteTickInfo: null,
            buffered: [],
            bufferedIndex: 0,
            allBuffers: true,
            allNoop: true,
            pendingcb: 0,
            prefinished: false,
            errorEmitted: false,
            emitClose: false,
            autoDestroy: false,
            errored: null,
            closed: false,
            closeEmitted: false
        },
        allowHalfOpen: true,
        _sockname: null,
        _pendingData: null,
        _pendingEncoding: '',
        server: Server {
            maxHeaderSize: undefined,
            insecureHTTPParser: undefined,
            _events: [Object: null prototype],
            _eventsCount: 2,
            _maxListeners: undefined,
            _connections: 1,
            _handle: [TCP],
            _usingWorkers: false,
            _workers: [],
            _unref: false,
            allowHalfOpen: true,
            pauseOnConnect: false,
            httpAllowHalfOpen: false,
            timeout: 0,
            keepAliveTimeout: 5000,
            maxHeadersCount: null,
            headersTimeout: 60000,
            requestTimeout: 0,
            _connectionKey: '6::::16988',
            [Symbol(IncomingMessage)]: [Function: IncomingMessage],
            [Symbol(ServerResponse)]: [Function: ServerResponse],
            [Symbol(kCapture)]: false,
            [Symbol(async_id_symbol)]: 9
        },
        _server: Server {
            maxHeaderSize: undefined,
            insecureHTTPParser: undefined,
            _events: [Object: null prototype],
            _eventsCount: 2,
            _maxListeners: undefined,
            _connections: 1,
            _handle: [TCP],
            _usingWorkers: false,
            _workers: [],
            _unref: false,
            allowHalfOpen: true,
            pauseOnConnect: false,
            httpAllowHalfOpen: false,
            timeout: 0,
            keepAliveTimeout: 5000,
            maxHeadersCount: null,
            headersTimeout: 60000,
            requestTimeout: 0,
            _connectionKey: '6::::16988',
            [Symbol(IncomingMessage)]: [Function: IncomingMessage],
            [Symbol(ServerResponse)]: [Function: ServerResponse],
            [Symbol(kCapture)]: false,
            [Symbol(async_id_symbol)]: 9
        },
        parser: HTTPParser {
            '0': [Function: bound setRequestTimeout],
            '1': [Function: parserOnHeaders],
            '2': [Function: parserOnHeadersComplete],
            '3': [Function: parserOnBody],
            '4': [Function: parserOnMessageComplete],
            '5': [Function: bound onParserExecute],
            '6': [Function: bound onParserTimeout],
            _headers: [],
            _url: '',
            socket: [Circular *1],
            incoming: [Circular *2],
            outgoing: null,
            maxHeaderPairs: 2000,
            _consumed: true,
            onIncoming: [Function: bound parserOnIncoming],
            [Symbol(resource_symbol)]: [HTTPServerAsyncResource]
        },
        on: [Function: socketListenerWrap],
        addListener: [Function: socketListenerWrap],
        prependListener: [Function: socketListenerWrap],
        _paused: false,
        _httpMessage: ServerResponse {
            _events: [Object: null prototype],
            _eventsCount: 2,
            _maxListeners: undefined,
            outputData: [],
            outputSize: 0,
            writable: true,
            destroyed: false,
            _last: false,
            chunkedEncoding: false,
            shouldKeepAlive: false,
            _defaultKeepAlive: true,
            useChunkedEncodingByDefault: true,
            sendDate: true,
            _removedConnection: false,
            _removedContLen: false,
            _removedTE: false,
            _contentLength: null,
            _hasBody: true,
            _trailer: '',
            finished: false,
            _headerSent: false,
            socket: [Circular *1],
            _header: null,
            _keepAliveTimeout: 5000,
            _onPendingData: [Function: bound updateOutgoingData],
            _sent100: false,
            _expect_continue: false,
            req: [Circular *2],
            locals: [Object],
            _startAt: undefined,
            _startTime: undefined,
            writeHead: [Function: writeHead],
            __onFinished: [Function],
            [Symbol(kCapture)]: false,
            [Symbol(kNeedDrain)]: false,
            [Symbol(corked)]: 0,
            [Symbol(kOutHeaders)]: [Object: null prototype]
        },
        _peername: { address: '::ffff:10.13.124.73', family: 'IPv6', port: 22969 },
        [Symbol(async_id_symbol)]: 51,
        [Symbol(kHandle)]: TCP {
            reading: true,
            onconnection: null,
            _consumed: true,
            [Symbol(owner_symbol)]: [Circular *1]
        },
        [Symbol(kSetNoDelay)]: false,
        [Symbol(lastWriteQueueSize)]: 0,
        [Symbol(timeout)]: null,
        [Symbol(kBuffer)]: null,
        [Symbol(kBufferCb)]: null,
        [Symbol(kBufferGen)]: null,
        [Symbol(kCapture)]: false,
        [Symbol(kBytesRead)]: 0,
        [Symbol(kBytesWritten)]: 0,
        [Symbol(RequestTimeout)]: undefined
    },
    _consuming: true,
    _dumped: false,
    next: [Function: next],
    baseUrl: '',
    originalUrl: '/installed',
    _parsedUrl: Url {
        protocol: null,
        slashes: null,
        auth: null,
        host: null,
        port: null,
        hostname: null,
        hash: null,
        search: null,
        query: null,
        pathname: '/installed',
        path: '/installed',
        href: '/installed',
        _raw: '/installed'
    },
    params: { '0': '/installed' },
    query: {},
    res: <ref *3> ServerResponse {
        _events: [Object: null prototype] {
            finish: [Array],
            end: [Function: onevent]
        },
        _eventsCount: 2,
        _maxListeners: undefined,
        outputData: [],
        outputSize: 0,
        writable: true,
        destroyed: false,
        _last: false,
        chunkedEncoding: false,
        shouldKeepAlive: false,
        _defaultKeepAlive: true,
        useChunkedEncodingByDefault: true,
        sendDate: true,
        _removedConnection: false,
        _removedContLen: false,
        _removedTE: false,
        _contentLength: null,
        _hasBody: true,
        _trailer: '',
        finished: false,
        _headerSent: false,
        socket: <ref *1> Socket {
            connecting: false,
            _hadError: false,
            _parent: null,
            _host: null,
            _readableState: [ReadableState],
            _events: [Object: null prototype],
            _eventsCount: 8,
            _maxListeners: undefined,
            _writableState: [WritableState],
            allowHalfOpen: true,
            _sockname: null,
            _pendingData: null,
            _pendingEncoding: '',
            server: [Server],
            _server: [Server],
            parser: [HTTPParser],
            on: [Function: socketListenerWrap],
            addListener: [Function: socketListenerWrap],
            prependListener: [Function: socketListenerWrap],
            _paused: false,
            _httpMessage: [Circular *3],
            _peername: [Object],
            [Symbol(async_id_symbol)]: 51,
            [Symbol(kHandle)]: [TCP],
            [Symbol(kSetNoDelay)]: false,
            [Symbol(lastWriteQueueSize)]: 0,
            [Symbol(timeout)]: null,
            [Symbol(kBuffer)]: null,
            [Symbol(kBufferCb)]: null,
            [Symbol(kBufferGen)]: null,
            [Symbol(kCapture)]: false,
            [Symbol(kBytesRead)]: 0,
            [Symbol(kBytesWritten)]: 0,
            [Symbol(RequestTimeout)]: undefined
        },
        _header: null,
        _keepAliveTimeout: 5000,
        _onPendingData: [Function: bound updateOutgoingData],
        _sent100: false,
        _expect_continue: false,
        req: [Circular *2],
        locals: {
            title: 'Bug Consolidator for Jira Heroku',
            addonKey: 'bug-consolidator-for-jira-heroku',
            clientKey: '',
            token: '',
            license: undefined,
            localBaseUrl: 'https://bcj.herokuapp.com',
            hostBaseUrl: '',
            hostUrl: '',
            hostStylesheetUrl: '/atlassian-connect/all.css',
            hostScriptUrl: 'https://connect-cdn.atl-paas.net/all.js'
        },
        _startAt: undefined,
        _startTime: undefined,
        writeHead: [Function: writeHead],
        __onFinished: [Function: listener] { queue: [Array] },
        [Symbol(kCapture)]: false,
        [Symbol(kNeedDrain)]: false,
        [Symbol(corked)]: 0,
        [Symbol(kOutHeaders)]: [Object: null prototype] {
            'x-powered-by': [Array],
            'strict-transport-security': [Array],
            'referrer-policy': [Array]
        }
    },
    _startAt: [ 48861, 836877392 ],
    _startTime: 2021-07-01T16:44:43.740Z,
    _remoteAddress: '::ffff:10.13.124.73',
    context: {
        http: null,
        title: 'Bug Consolidator for Jira Heroku',
        addonKey: 'bug-consolidator-for-jira-heroku',
        clientKey: '',
        token: '',
        license: undefined,
        localBaseUrl: 'https://bcj.herokuapp.com',
        hostBaseUrl: '',
        hostUrl: '',
        hostStylesheetUrl: '/atlassian-connect/all.css',
        hostScriptUrl: 'https://connect-cdn.atl-paas.net/all.js'
    },
    payload: {
        hostScriptUrl: 'https://connect-cdn.atl-paas.net/all.js',
        http: null,
        title: 'Bug Consolidator for Jira Heroku',
        addonKey: 'bug-consolidator-for-jira-heroku',
        clientKey: '',
        token: '',
        license: undefined,
        localBaseUrl: 'https://bcj.herokuapp.com',
        hostBaseUrl: '',
        hostUrl: '',
        hostStylesheetUrl: '/atlassian-connect/all.css'
    },
    body: {
        key: 'bug-consolidator-for-jira-heroku',
        clientKey: '4c72d0bb-d200-332f-8d42-6e7b66f88ccb',
        publicKey: 'MIGf... "Center part removed for privacy" ...AQAB',
        sharedSecret: 'L1BD... "Center part removed for privacy" ...OLzA',
        serverVersion: '100167',
        pluginsVersion: '1001.0.0-SNAPSHOT',
        baseUrl: 'https://hiproject.atlassian.net',
        productType: 'jira',
        description: 'Atlassian JIRA at https://hiproject.atlassian.net ',
        eventType: 'installed'
    },
    _body: true,
    length: undefined,
    route: Route { path: '*', stack: [ [Layer] ], methods: { post: true } },
    [Symbol(kCapture)]: false,
    [Symbol(RequestTimeout)]: undefined
}

I could use help logging the lifecycle transaction without braking the app and trapping the error.

@RhysDiab Seeing this is your baby I´m struggling with. Do you happen to have insights into this problem of mine. All help small or big appreciated.

B.r. Halli.

I’m not sure if I understand the context of the full req that you logged. I’m guessing that’s with logging, not the default route. Then the problem in your override is that it does not return a 201 or 204.

Whether it is the default or your override, what’s interesting is the point about, “Sending POST to /installed using Postman gives the same result (503)”. That means Heroku is acting as some kind of HTTP gateway, it detects the timeout (or null status code), and sends 503 for you. I think we need to focus on getting the correct 201 or 204 from /installed and for that we don’t need the log/trap.

I don’t know Heroku well enough to ask more specific questions. Is the $PORT correctly mapped to 443? And what is doing the SSL termination for HTTPS? I’m wondering if the GET requests on atlassian-connect.json were HTTP or HTTPS?

1 Like
"store": {
            // Don't use memory storage for production, otherwise
            // data in the storage will go away when the app server restarts.
            // Here, we use PostgreSQL:
            "type": "postgres",
            // PaaS like Heroku will provide DB connection URL through environment variable.
            "url": "$DATABASE_URL",
            **"dialectOptions": {**
**                "ssl": {**
**                    "require": true,**
**                    "rejectUnauthorized": false**
**                }**
**            }**
        },

Try uncommenting the dialect options. I suspect postgres is encountering an error and I remember needing to add these options a while back.

Make sure you’re using the latest version of ACE as one of the early v7 versions had some bugs in it. Check again if there’s another update.

Also you may need to add "signed-install": "enable" to your config if you’re using the latest version of ACE. See here.

I’d also add that since Atlassian Connect Express added support for react.js and the release of Forge I stopped updating the Atlassian Connect Next starter kit. I still run my apps using Atlassian Connect Next on heroku, but will likely be migrating to Forge at some point. If you’re still early in the dev process I recommend trying to use Forge. I’ll update the readme of Atlassian Connect Next now.

2 Likes

Thanks again @ibuchanan and @RhysDiab for your effort.

I have now set up a dev postgres db and tried registering the app in dev mode. The results I get now are similar but not the same. Now the actual request gets a timeout when I send POST /installed from Postman. As when POST-ing to Heroku, Heroku sent a timeout response back which confirms your theory @ibuchanan.

If I send an empty POST /installed request I get: 401 “No baseUrl provided in registration info.” and the terminal logs:

POST /installed 401 2.114 ms - 41

but If I send a POST /installed including a JSON body the request hangs and the terminal logs eventually:

POST /installed - - ms - -

Now with Heroku out of the registration process I am left with the question how is it possible to get ACE to hang? And how can I troubleshoot it?

As to your suggestions @RhysDiab I had already uncommented the dialectOptions settings and I´m running the latest ACE module.

Below I put the latest versions of package.json, config.json and atlassian-connect.json

Again I want to send my thanks as I appreciate your help tremendously.

B.r. Halli

package.json

{
  "name": "custom-server-express",
  "version": "1.0.0",
  "scripts": {
    "dev": "AC_OPTS=no-auth,no-token-verification,force-reg node -r esm server.js",
    "backend": "nodemon -r esm server.js",
    "build": "next build",
    "start": "NODE_ENV=production node -r esm server.js",
    "start-debug": "NODE_ENV=production node --inspect -r esm server.js"
  },
  "dependencies": {
    "@atlaskit/avatar": "^20.3.1",
    "@atlaskit/button": "^15.1.6",
    "@atlaskit/icon": "^21.6.0",
    "@atlaskit/theme": "^11.2.1",
    "@sentry/browser": "^5.15.4",
    "antd": "^4.1.1",
    "atlassian-connect-express": "^7.3.0",
    "body-parser": "^1.19.0",
    "compression": "^1.7.4",
    "cookie-parser": "^1.4.4",
    "cross-env": "^5.2.0",
    "dayjs": "^1.8.30",
    "dotenv": "^8.2.0",
    "errorhandler": "^1.5.1",
    "esm": "^3.2.25",
    "express": "^4.17.1",
    "helmet": "^3.21.2",
    "http": "0.0.0",
    "morgan": "^1.9.1",
    "next": "^9.5.5",
    "node-fetch": "^2.6.1",
    "nodemon": "^2.0.3",
    "os": "^0.1.1",
    "pg": "^7.10.0",
    "react": "^16.7.0",
    "react-dom": "^16.7.0",
    "sequelize": "^5.21.2",
    "sqlite3": "^5.0.2",
    "styled-components": "^5.1.0"
  },
  "devDependencies": {
    "ngrok": "^3.2.7"
  }
}

config.json

// It's OK to write comments in this JSON configuration file,
// comments will be stripped when this file is loaded.
{
    // "development" is the default environment.
    // To change, set NODE_ENV (http://expressjs.com/api.html#app.configure).
    "development": {
        // Port the Express server will listen on.
        "port": 3000,
        // Use views/unauthorized.hbs for error page.
        "errorTemplate": true,
        // atlassian-connect-express currently integrates with Sequelize for
        // persistence to store the host client information (i.e. client key,
        // host public key etc.). When no adapter is specified, it defaults to
        // memory.
        //
        // To specify a storage other than memory, set
        // "dialect" to one of Sequelize's other supported dialects:
        // http://docs.sequelizejs.com/manual/installation/usage.html#dialects
        //
        // To use a custom storage adapter, configure it in "store".
        // Make sure to register the adapter factory in app.js:
        //
        //   ace.store.register(adapterName, factoryFunction)
        //
        // At https://bitbucket.org/atlassian/atlassian-connect-express/src/master/lib/store/,
        // see index.js and sequelize.js for code demonstrating how to write
        // a conformant adapter. The default values are as follows:
        //
        //   "store": {
        //     "adapter": "sequelize",
        //     "dialect": "sqlite3",
        //     "storage": ":memory:"
        //   },
        //
        // To configure PostgreSQL, the following can be used:
        //
          "store": {
            "adapter": "sequelize",
            "dialect": "postgres",
            "url": "$DATABASE_URL"
          }
        //
        // An appropriate DB driver for Sequelize is required for storage other than memory.
        // For PostgreSQL, run the following command:
        //
        //   npm install --save pg
        // "store": {
        //     "adapter": "sequelize",
        //     "dialect": "sqlite3",
        //     "type": "memory"
        // }
    },
    // Configuration for production, which is enabled by setting
    // the NODE_ENV=production environment variable.
    "production": {
        // PaaS like Heroku will provide HTTP port via environement variable.
        "port": "$PORT",
        // Use views/unauthorized.hbs for error page.
        "errorTemplate": true,
        // Public URL to production app.
        "localBaseUrl": "https://bcj.herokuapp.com",
        "store": {
            // Don't use memory storage for production, otherwise
            // data in the storage will go away when the app server restarts.
            // Here, we use PostgreSQL:
            "dialect": "postgres",
            // PaaS like Heroku will provide DB connection URL through environment variable.
            "url": "$DATABASE_URL"
            // Bætti við í samræmi við tillögu af vef: https://stackoverflow.com/questions/25000183/node-js-postgresql-error-no-pg-hba-conf-entry-for-host/64960461#64960461
            // "dialectOptions": {
            //      "ssl": {
            //         "require":true,
            //         "rejectUnauthorized": false
            //     }
            // }
        },
        // The app can only be registered by the products on these domains:
        "whitelist": [
            "*.jira-dev.com",
            "*.atlassian.net",
            "*.atlassian.com",
            "*.jira.com"
        ]
    },
    "signed-install": "enable",
    "product": "jira"
}

atlassian-connect.json

{
    "key": "bug-consolidator-for-jira-heroku",
    "name": "Bug Consolidator for Jira Heroku",
    "description": "My Next.js Atlassian App",
    "baseUrl": "https://a8a92919461d.ngrok.io/",
    "links": {
        "self": "https://a8a92919461d.ngrok.io//atlassian-connect.json"
    },
    "authentication": {
        "type": "jwt"
    },
    "lifecycle": {
        "installed": "/installed"
    },
    "scopes": [
        "READ",
        "WRITE"
    ],
    "modules": {
        "generalPages": [
            {
                "key": "landing",
                "location": "system.top.navigation.bar",
                "name": {
                    "value": "dev-heroku"
                },
                "url": "/landing",
                "conditions": [
                    {
                        "condition": "user_is_logged_in"
                    }
                ]
            }
        ]
    },
    "apiMigrations": {
        "signed-install": true
    }
}

@HallbjrnMagnsson

Check the list of suggestions I’ve provided to other community member recently. Maybe one will get you back on the path. E.g. launching Node with DEBUG in dev mode to confirm all the necessary routes are set up (by add-on).

BTW in dev mode I launch application in a way as I would on Production (e.g. not using any AC_OPTS, whereas for PR I would explicitly set NODE_ENV to ‘production’) – this way I can feel more confident that it would match Production behaviour.

Based on the described symptoms somehow I suspect that it could be related to Postgres (i.e. /installed will attempt to store clientInfo in add-on settings and in case it cannot connect – it could be causing the timeout). Try setting up settings store using Sqlite or Memory session storage (instead of Postgres) to confirm/eliminate. I am using Sqlite on production - it is just a single file with one table after all (easy to copy between version updates and application restarts).

Also looked into Heroku - Request Timeout article. It states that Heroku expects application to respond within 30s, otherwise the router will terminate the request. I suggest you to read this article to understand Heroku behaviour and their expectations of your application for handling requests. Addressing H12 Errors (Request-timeouts) is another good article to get familiar with.

I hope that above will help you. If not - let’s dig deeper :slight_smile:

2 Likes

I am very happy to announce that we now have a working app on Heroku :slight_smile:

We switch to MySQL instead of Postgres and with a few minor issues we got it working fairly quickly.

We used the node module mysql2, set the dialect to mysql, correct db url and user credentials in env. variables.

In Heroku we used the JawsDB MySQL add-on

I and the team I´m working with want to thank you all again for your help @ViliusZigmantas, @ibuchanan & @RhysDiab.

2 Likes