Skip to content
Open
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
18 changes: 17 additions & 1 deletion bin/Hubot.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const switches = [
['-f', '--file HUBOT_FILE', 'Path to adapter file, e.g. "./adapters/CustomAdapter.mjs"'],
['-c', '--create HUBOT_CREATE', 'Create a deployable hubot'],
['-d', '--disable-httpd HUBOT_HTTPD', 'Disable the HTTP server'],
['-s', '--httpdserver HUBOT_HTTPD_SERVER', 'An async factory function that receives this robot (via options) and returns or creates an HttpServerPort instance. Defaults to a factory that instantiates HttpServerPort'],
['-h', '--help', 'Display the help information'],
['-l', '--alias HUBOT_ALIAS', "Enable replacing the robot's name with alias"],
['-n', '--name HUBOT_NAME', 'The name of the robot in chat'],
Expand All @@ -28,7 +29,8 @@ const options = {
scripts: process.env.HUBOT_SCRIPTS || [],
name: process.env.HUBOT_NAME || 'Hubot',
file: process.env.HUBOT_FILE,
configCheck: false
configCheck: false,
httpdServer: process.env.HUBOT_HTTPD_SERVER,
}

const Parser = new OptParse(switches)
Expand All @@ -51,6 +53,10 @@ Parser.on('disable-httpd', opt => {
options.enableHttpd = false
})

Parser.on('httpdserver', (opt, value) => {
options.httpdServer = value
})

Parser.on('help', function (opt, value) {
console.log(Parser.toString())
return process.exit(0)
Expand Down Expand Up @@ -99,6 +105,16 @@ if (options.file) {
options.adapter = options.file.split('/').pop().split('.')[0]
}

if (options.enableHttpd === true && !options.httpdServer) {
options.httpdServer = 'src/ports/ExpressHttpServerPort.mjs'
}

if (options.httpdServer) {
const httpdServerModulePath = pathResolve('.', options.httpdServer)
const httpdServerModule = await import(httpdServerModulePath)
options.enableHttpd = httpdServerModule.default
}

const robot = Hubot.loadBot(options.adapter, options.enableHttpd, options.name, options.alias)
export default robot

Expand Down
31 changes: 19 additions & 12 deletions src/Robot.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Response from './Response.mjs'
import { Listener, TextListener } from './Listener.mjs'
import Message from './Message.mjs'
import Middleware from './Middleware.mjs'
import { HttpServerPort } from './ports/HttpServerPort.mjs'

const File = fs.promises
const HUBOT_DEFAULT_ADAPTERS = ['Campfire', 'Shell']
Expand All @@ -24,7 +25,7 @@ class Robot {
// dispatch them to matching listeners.
//
// adapter - A String of the adapter name.
// httpd - A Boolean whether to enable the HTTP daemon.
// httpd - An async function that creates the HTTP daemon. Defaults to HttpServerPort, which doesn't do anything.
Comment thread
joeyguerra marked this conversation as resolved.
// name - A String of the robot name, defaults to Hubot.
// alias - A String of the alias of the robot name
constructor (adapter, httpd, name, alias) {
Expand All @@ -48,7 +49,17 @@ class Robot {
this.adapterName = adapter ?? this.adapterName
}

this.shouldEnableHttpd = httpd ?? true
this.httpDServerFactory = async (options) => new HttpServerPort(options)
if (httpd && typeof httpd === 'function') {
this.httpDServerFactory = httpd
}

if (httpd === true) {
this.httpDServerFactory = Robot.setupExpress
}

this.server = null
this.router = null
this.datastore = null
this.Response = Response
this.commands = []
Expand Down Expand Up @@ -436,7 +447,7 @@ class Robot {
// Setup the Express server's defaults.
//
// Returns Server.
async setupExpress () {
static async setupExpress (robot) {
const user = process.env.EXPRESS_USER
const pass = process.env.EXPRESS_PASSWORD
const stat = process.env.EXPRESS_STATIC
Expand Down Expand Up @@ -469,10 +480,10 @@ class Robot {
}
return new Promise((resolve, reject) => {
try {
this.server = app.listen(port, address, () => {
this.router = app
this.emit('listening', this.server)
resolve(this.server)
robot.server = app.listen(port, address, () => {
robot.router = app
robot.emit('listening', robot.server)
resolve(robot.server)
})
} catch (err) {
reject(err)
Expand Down Expand Up @@ -661,11 +672,7 @@ class Robot {
//
// Returns whatever the adapter returns.
async run () {
if (this.shouldEnableHttpd) {
await this.setupExpress()
} else {
this.setupNullRouter()
}
await this.httpDServerFactory(this)
await this.adapter.run()
this.emit('running')
}
Expand Down
68 changes: 68 additions & 0 deletions src/ports/ExpressHttpServerPort.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { HttpServerPort } from './HttpServerPort.mjs'
import express from 'express'
import basicAuth from 'express-basic-auth'

class ExpressHttpServerPort extends HttpServerPort {
get address() {
return this.robot.server.address()
}
async start(port) {
const user = process.env.EXPRESS_USER
const pass = process.env.EXPRESS_PASSWORD
const stat = process.env.EXPRESS_STATIC
const address = process.env.EXPRESS_BIND_ADDRESS || process.env.BIND_ADDRESS || '0.0.0.0'
const limit = process.env.EXPRESS_LIMIT || '100kb'
const paramLimit = parseInt(process.env.EXPRESS_PARAMETER_LIMIT) || 1000
const app = express()

app.use((req, res, next) => {
res.setHeader('X-Powered-By', `hubot/${encodeURI(this.robot.name)}`)
return next()
})

if (user && pass) {
const authUser = {}
authUser[user] = pass
app.use(basicAuth({ users: authUser }))
}

app.use(express.json({ limit }))
app.use(express.urlencoded({ limit, parameterLimit: paramLimit, extended: true }))

if (stat) {
app.use(express.static(stat))
}
return new Promise((resolve, reject) => {
try {
this.robot.server = app.listen(port, address, () => {
this.robot.router = app
this.robot.emit('listening', this.robot.server)
resolve(this.robot.server)
})
} catch (err) {
reject(err)
}
})
}
Comment thread
joeyguerra marked this conversation as resolved.
Comment thread
joeyguerra marked this conversation as resolved.

async stop() {
if (!this.robot || !this.robot.server) {
return
}
return new Promise((resolve, reject) => {
this.robot.server.close(err => {
if (err) {
return reject(err)
}
resolve()
})
})
}
}

export default async (robot) => {
const server = new ExpressHttpServerPort(robot)
const port = process.env.EXPRESS_PORT || process.env.PORT || 8080
await server.start(port)
return server
}
29 changes: 29 additions & 0 deletions src/ports/HttpServerPort.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class HttpServerPort {
constructor(robot) {
this.robot = robot
}
async stop() {}
async start(port) {
this.port = port
this.robot.server = {}
this.robot.router = this
return this.robot.server
}
address() {
return { port: this.port }
}
use(handler) {}
get(path, handler) {}
post(path, handler) {}
put(path, handler) {}
patch(path, handler) {}
query(path, handler) {}
delete(path, handler) {}
trace(path, handler) {}
options(path, handler) {}
head(path, handler) {}
Comment on lines +15 to +24

Copilot AI Jan 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTTP method stubs (get, post, put, etc.) don't match the Express API signature. Express router methods return the app/router instance for chaining (e.g., app.get('/path', handler).post('/path', handler)), but these empty methods return undefined. Consider returning this to support method chaining for compatibility with Express patterns.

Suggested change
use(handler) {}
get(path, handler) {}
post(path, handler) {}
put(path, handler) {}
patch(path, handler) {}
query(path, handler) {}
delete(path, handler) {}
trace(path, handler) {}
options(path, handler) {}
head(path, handler) {}
use(handler) {
return this
}
get(path, handler) {
return this
}
post(path, handler) {
return this
}
put(path, handler) {
return this
}
patch(path, handler) {
return this
}
query(path, handler) {
return this
}
delete(path, handler) {
return this
}
trace(path, handler) {
return this
}
options(path, handler) {
return this
}
head(path, handler) {
return this
}

Copilot uses AI. Check for mistakes.
}

export {
HttpServerPort
}
2 changes: 1 addition & 1 deletion test/Hubot_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('Running bin/Hubot.mjs', () => {
-f, --file HUBOT_FILE
-c, --create HUBOT_CREATE
-d, --disable-httpd HUBOT_HTTPD
-s, --httpdserver HUBOT_HTTPD_SERVER
-h, --help
-l, --alias HUBOT_ALIAS
-n, --name HUBOT_NAME
Expand Down Expand Up @@ -112,7 +113,6 @@ describe('Running hubot with args', () => {
} finally {
hubot.kill()
}
console.log(actual)
assert.ok(actual instanceof TypeError)
assert.deepEqual(actual.message, 'fetch failed', 'this is an expected failure since the web service should not be running')
done()
Expand Down
40 changes: 40 additions & 0 deletions test/Robot_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import { describe, it, beforeEach, afterEach } from 'node:test'
import assert from 'node:assert/strict'
import path from 'node:path'
import http from 'node:http'
Comment thread
joeyguerra marked this conversation as resolved.
import { Robot, CatchAllMessage, EnterMessage, LeaveMessage, TextMessage, TopicMessage, User, Response } from '../index.mjs'
import mockAdapter from './fixtures/MockAdapter.mjs'
import { HttpServerPort } from '../src/ports/HttpServerPort.mjs'

describe('Robot', () => {
describe('#http', () => {
Expand Down Expand Up @@ -1041,4 +1043,42 @@ describe('Robot', () => {
delete process.env.PORT
})
})

describe('Non-express HTTP server', async () => {
it('should work with a non-express http server', async () => {
class HttpServer extends HttpServerPort {
async start(port) {
return new Promise((resolve, reject) => {
try {
this.robot.server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('Hello World\n')
})
this.robot.router = this
this.robot.server = this.robot.server.listen(port, '127.0.0.1', () => {
this.robot.emit('listening', this.robot.server)
resolve(this.robot.server)
})
} catch (err) {
reject(err)
}
})
}
}
const httpDServerFactory = async (robot) => {
const server = new HttpServer(robot)
await server.start(process.env.PORT || 0)
return server
}
const robot = new Robot(mockAdapter, httpDServerFactory, 'TestHubot')

Copilot AI Jan 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable httpDServerFactory is referenced but never defined. This test will fail when executed.

Copilot uses AI. Check for mistakes.
await robot.loadAdapter()
await robot.run()
const port = robot.server.address().port
const res = await fetch(`http://127.0.0.1:${port}/`)
assert.equal(res.status, 200)
assert.equal(await res.text(), 'Hello World\n')
robot.shutdown()
delete process.env.PORT
})
})
})
36 changes: 36 additions & 0 deletions test/contracts/HttpServer_test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { HttpServerPort } from '../../src/ports/HttpServerPort.mjs'

test('HttpServerPort API', async t => {
await t.test('start and stop', async () => {
const server = new HttpServerPort({})
await server.start(0)
assert.equal(server.address().port, 0)
await server.stop()
})

await t.test('use', async () => {
const server = new HttpServerPort({})
server.use((req, res) => {
res.end('Hello, World!')
})
await server.start(0)
await server.stop()
})

await t.test('http methods', async () => {
const server = new HttpServerPort({})
await server.start(0)
assert.ok(server.get)
assert.ok(server.post)
assert.ok(server.put)
assert.ok(server.patch)
assert.ok(server.query)
assert.ok(server.delete)
assert.ok(server.trace)
assert.ok(server.options)
assert.ok(server.head)
await server.stop()
})
})
Comment thread
joeyguerra marked this conversation as resolved.
Loading