Search code examples
node.jsbotframeworkmicrosoft-teamsadaptive-cardsweb-chat

SOLVED: Getting onTurnError when sending adaptive card to MS Teams using Bot Framework SDK 4


I'm developing a bot using Bot Framework SDK 4 with Node.js as an MS-Teams app. The bot is properly registered in Azure but hosted locally with Ngrok forwarding for testing purposes.

The goal of the bot is to do external requests to an api and then populate an adaptive card template from the response of the api for a more engaging interface than text.

This template was created with the Adaptive Card Designer, where it rendered just fine. Testing the bot with the Emulator works too. Same with the Web Chat channel, see screenshots. However when installed in teams, the card is not shown and an error is thrown.

Emulator Output

WebChat Output

The Errors I get when chatting in Teams:

  • Instead of the card, I get a message "The bot encountered an error or bug. To continue to run this bot, please fix the bot source code." This is thrown by adapter.onTurnError, which is integrated in all Microsoft samples, which this bot is based on too.
  • In the console where the bot is running it says: "[onTurnError] unhandled error: Error: Unknown".
  • In the channel overview on Azure there appears a timestamp and the message: "Unknown" for the Teams channel.

All other dialog and messages work just fine, this error just happens when I send the adaptive card as an attachment to an activity.

Next I'll paste some code snippets to give some context to what I tried to do

The template I created:

{
    "type": "AdaptiveCard",
    "$schema": "https://adaptivecards.io/schemas/1.2.0/adaptive-card.json",
    "version": "1.2",
    "contentType": "application/vnd.microsoft.card.adaptive",
    "speak": "not set",
    "body": [
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "width": "auto",
                    "items": [
                        {
                            "type": "TextBlock",
                            "size": "Large",
                            "text": "${title}",
                            "weight": "Bolder"
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "${type}",
                            "color": "Accent",
                            "height": "stretch",
                            "fontType": "Default",
                            "size": "Large",
                            "weight": "Lighter"
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Private",
                            "horizontalAlignment": "Right",
                            "height": "stretch",
                            "size": "Large",
                            "color": "Warning",
                            "$when": "${$root.private == '1'}"
                        }
                    ]
                }
            ]
        },
        {
            "type": "TextBlock",
            "spacing": "None",
            "text": "Created: {{DATE(${createdUtc},SHORT)}}",
            "isSubtle": true,
            "size": "Small",
            "weight": "Lighter",
            "wrap": true,
            "$when": "${$root.createdSet == '1'}"
            
        },
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "width": "auto",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Creator",
                            "weight": "Bolder",
                            "color": "Accent"
                        },
                        {
                            "type": "TextBlock",
                            "weight": "Bolder",
                            "text": "${creator.name}",
                            "wrap": true,
                            "spacing": "None"
                        }
                    ],
                    "spacing": "Medium"
                },
                {
                    "type": "Column",
                    "width": "auto",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Assignee",
                            "weight": "Bolder",
                            "color": "Accent"
                        },
                        {
                            "type": "TextBlock",
                            "weight": "Bolder",
                            "wrap": true,
                            "spacing": "None",
                            "text": "${$root.assignee.name}"
                        }
                    ],
                    "spacing": "Medium"
                },
                {
                    "type": "Column",
                    "width": "auto",
                    "spacing": "Medium",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Priority",
                            "weight": "Bolder",
                            "color": "Accent"
                        },
                        {
                            "type": "TextBlock",
                            "weight": "Bolder",
                            "text": "${priority}",
                            "wrap": true,
                            "spacing": "None"
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "auto",
                    "spacing": "Medium",
                    "items": [
                        {
                            "type": "TextBlock",
                            "weight": "Bolder",
                            "color": "Accent",
                            "text": "Status"
                        },
                        {
                            "type": "TextBlock",
                            "weight": "Bolder",
                            "text": "${status}",
                            "wrap": true,
                            "spacing": "None"
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Due Date",
                            "weight": "Bolder",
                            "color": "Accent"
                        },
                        {
                            "type": "TextBlock",
                            "weight": "Bolder",
                            "text": "{{DATE(${dueUTC},SHORT)}}",
                            "wrap": true,
                            "spacing": "None"
                        }
                    ],
                    "spacing": "Medium",
                    "$when": "${$root.status!= 'Done' && ${$root.dueSet == '1'}}"
                },
                {
                    "type": "Column",
                    "width": "stretch",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Done Date",
                            "weight": "Bolder",
                            "color": "Accent"
                        },
                        {
                            "type": "TextBlock",
                            "weight": "Bolder",
                            "text": "{{DATE(${doneUTC},SHORT)}}",
                            "wrap": true,
                            "spacing": "None"
                        }
                    ],
                    "spacing": "Medium",
                    "$when": "${$root.status== 'Done' && ${$root.doneSet == '1'}}"
                }
            ],
            "spacing": "Small",
            "separator": true
        },
        {
            "type": "TextBlock",
            "text": "${description}",
            "wrap": true,
            "separator": true
        }
    ],
    "actions": [
        {
            "type": "Action.OpenUrl",
            "title": "View",
            "url": "${viewUrl}"
        }
    ]
    
}

I populate the template variables like this in my issueCardGenerator class:

("issue" is an object containing information I'd like to send to the user as an adaptive card. "issueCard" is a reference to the json template. The specifics on how I parse the information does not matter too much here. "translate" for example translates some enum's of the issue object to natural language.)

generateIssueCard(issue){
        //render card
        var template = new Template(issueCard);
            
        var cardPayload = template.expand({
            $root: {
                title: issue.title==null ? "No Title available" : issue.title,
                description: issue.description==null ? "": issue.description, 
                creator: {
                    name: issue.creator==null ? "Not Set" : issue.creator.username 
                },
                assignee: {
                    name: issue.assignee==null ? "Not Set" : issue.assignee.username 
                },
                type: issue.type==null ? "" : this.translate(issue.type.value),
                status: issue.status==null ? "No Status" : this.translate(issue.status.value),
                priority: issue.priority==null ? "No Priority" : this.translate(issue.priority.value),

                //only display times when they are set
                dueSet: issue.due_date==null ? "0" : "1",
                doneSet: issue.done_date==null ? "0" : "1",
                createdSet: issue.created==null ? "0" : "1",

                //convert time format
                dueUtc: issue.due_date==null ? "No Due Date" : moment(issue.due_date).format('YYYY-MM-DD')+'T06:08:39Z',
                createdUtc: issue.created==null ? "No Creation Time" : moment(issue.created).format('YYYY-MM-DD')+'T06:08:39Z',
                doneUtc: issue.done_date==null ? "No Done Date" : moment(issue.done_date).format('YYYY-MM-DD')+'T06:08:39Z',

                viewUrl: "https://www.some-url.com",
                private: issue.private==null ? "0" :issue.private
            }
         });

        var adaptiveCard = new AdaptiveCard();
        adaptiveCard.parse(cardPayload);
        return(CardFactory.adaptiveCard(adaptiveCard))

    }

And then send it like this in my dialog-step, which is part of a waterfall dialog:

(The user inputs an "idInput" to sepcify the id of the issue which is then requested and saved in "issue")

async summaryStep(stepContext) {

    const idInput = stepContext.result;
    if (idInput) {
        var issue;

        //do request and set issue variable here. This works fine and shows proper in the logs
        issue= await this.doRequest(url,accessToken,"GET");
        
        var cardGenerator=new IssueCardGenerator();

        var activity= {
            text: 'This is your requested issue with the id ' +idInput +': ',
            attachments: [cardGenerator.generateIssueCard(issue)]
        }

        await stepContext.context.sendActivity(activity);

    }
    return await stepContext.endDialog();

Solution

  • Solved the "problem"

    Here is the solution if anyone stumbles onto the same issue:

    Turns out in my CardGenerator...

    var adaptiveCard = new AdaptiveCard();
    adaptiveCard.parse(cardPayload);
    return(CardFactory.adaptiveCard(adaptiveCard))
    

    ..needs to be changed to use the payload directly for the cardfactory:

    return(CardFactory.adaptiveCard(cardPayload))
    

    Why the other channels handle this properly with the first variant and Teams does not, I don't know and probably never will