Challenge #7: Unexpected Status

Good code works. Great code handles errors.

Currently, Rot13Client.transformAsync() assumes that nothing can go wrong: that the ROT-13 service is always up, running, and providing meaningful responses.

Of course, anyone who’s worked with networked systems knows that perfection is impossible. Something will go wrong... if not today, then tomorrow. In this challenge, you’ll handle the first of those situations, and fail gracefully when the ROT-13 service returns an unexpected status code. This is the Paranoic Telemetry pattern.

Instructions

1. Implement the "fails gracefully when status code has unexpected value" test (back up at the top):

  1. Simulate the ROT-13 service returning a 400 (Bad Request) error when you call rot13Client.transformAsync().
  2. Assert that rot13Client.transformAsync() throws the following error:
    const expectedError =
    	"Unexpected status from ROT-13 service\n" +
    	`Host: ${HOST}:9999\n` +
    	"Endpoint: /rot13/transform\n" +
    	"Status: 400\n" +
    	`Headers: ${JSON.stringify(VALID_ROT13_HEADERS)}\n` +
    	`Body: ${VALID_ROT13_BODY}`;

2. Modify rot13Client.transformAsync():

  1. Throw the above error when the ROT-13 service responds with any status other than 200 (OK).

Remember to commit your changes when you’re done.

API Documentation

throw new Error(message);

Throw an exception.

  • message (string) - the error message
await assert.throwsAsync(fnAsync, expectedError);

Assert that an asynchronous function throws an error. Use it like this:

await assert.throwsAsync(
	() => transformAsync(...),    // note: NO await here
	"my expected error",
);
  • fnAsync (function) - the function to run
  • expectedError (string) - the error fnAsync is expected to throw

JavaScript Primers

No new concepts.

Hints

1
Your test needs to set up the HTTP response and call rot13Client.transformAsync().
You can use the transformAsync() test helper for that. Don’t forget to await it.
Remember to specify everything that’s in the error message.
it("fails gracefully when status code has unexpected value", async () => {
	await transformAsync({
		port: 9999,
		rot13ServiceStatus: 400,
		rot13ServiceHeaders: VALID_ROT13_HEADERS,
		rot13ServiceBody: VALID_ROT13_BODY,
	});
});
2
Your test needs to assert that transformAsync() throws an error.
You can use assert.throwsAsync() to check for that. Don’t forget to await it. (You’ll remove the await from the transformAsync() call.)
it("fails gracefully when status code has unexpected value", async () => {
	const expectedError =
		"Unexpected status from ROT-13 service\n" +
		`Host: ${HOST}:9999\n` +
		"Endpoint: /rot13/transform\n" +
		"Status: 400\n" +
		`Headers: ${JSON.stringify(VALID_ROT13_HEADERS)}\n` +
		`Body: ${VALID_ROT13_BODY}`;

	await assert.throwsAsync(
		() => transformAsync({
			port: 9999,
			rot13ServiceStatus: 400,
			rot13ServiceHeaders: VALID_ROT13_HEADERS,
			rot13ServiceBody: VALID_ROT13_BODY,
		}),
		expectedError,
	);
});
3
The test is ready to run.
It should fail with "Expected exception".

That’s because the production code isn’t throwing an exception.

4
rot13Client.transformAsync() should throw an exception based on the status code.
You can get the status code from response.status.
async transformAsync(port, text, correlationId) {
	this._listener.emit({ port, text, correlationId });

	const response = await this._httpClient.requestAsync({
		host: HOST,
		port,
		method: "POST",
		path: "/rot13/transform",
		headers: {
			"content-type": "application/json",
			"x-correlation-id": correlationId,
		},
		body: JSON.stringify({ text }),
	});

	if (response.status !== 200) {
		throw new Error(
			"Unexpected status from ROT-13 service\n" +
			`Host: ${HOST}:${port}\n` +
			`Endpoint: ${TRANSFORM_ENDPOINT}\n` +
			`Status: ${response.status}\n` +
			`Headers: ${JSON.stringify(response.headers)}\n` +
			`Body: ${response.body}`
		);
	}

	const parsedBody = JSON.parse(response.body);
	return parsedBody.transformed;
}

Complete Solution

Test code:
it("fails gracefully when status code has unexpected value", async () => {
	const expectedError =
		"Unexpected status from ROT-13 service\n" +
		`Host: ${HOST}:9999\n` +
		"Endpoint: /rot13/transform\n" +
		"Status: 400\n" +
		`Headers: ${JSON.stringify(VALID_ROT13_HEADERS)}\n` +
		`Body: ${VALID_ROT13_BODY}`;

	await assert.throwsAsync(
		() => transformAsync({
			port: 9999,
			rot13ServiceStatus: 400,
			rot13ServiceHeaders: VALID_ROT13_HEADERS,
			rot13ServiceBody: VALID_ROT13_BODY,
		}),
		expectedError,
	);
});
Production code (JavaScript):
async transformAsync(port, text, correlationId) {
	this._listener.emit({ port, text, correlationId });

	const response = await this._httpClient.requestAsync({
		host: HOST,
		port,
		method: "POST",
		path: "/rot13/transform",
		headers: {
			"content-type": "application/json",
			"x-correlation-id": correlationId,
		},
		body: JSON.stringify({ text }),
	});

	if (response.status !== 200) {
		throw new Error(
			"Unexpected status from ROT-13 service\n" +
			`Host: ${HOST}:${port}\n` +
			`Endpoint: ${TRANSFORM_ENDPOINT}\n` +
			`Status: ${response.status}\n` +
			`Headers: ${JSON.stringify(response.headers)}\n` +
			`Body: ${response.body}`
		);
	}

	const parsedBody = JSON.parse(response.body);
	return parsedBody.transformed;
}
Production code (TypeScript):
async transformAsync(
	port: number,
	text: string,
	correlationId: string,
): Promise<string> {
	this._listener.emit({ port, text, correlationId });

	const response = await this._httpClient.requestAsync({
		host: HOST,
		port,
		method: "POST",
		path: "/rot13/transform",
		headers: {
			"content-type": "application/json",
			"x-correlation-id": correlationId,
		},
		body: JSON.stringify({ text }),
	});

	if (response.status !== 200) {
		throw new Error(
			"Unexpected status from ROT-13 service\n" +
			`Host: ${HOST}:${port}\n` +
			`Endpoint: ${TRANSFORM_ENDPOINT}\n` +
			`Status: ${response.status}\n` +
			`Headers: ${JSON.stringify(response.headers)}\n` +
			`Body: ${response.body}`
		);
	}

	const parsedBody = JSON.parse(response.body);
	return parsedBody.transformed;
}

Next challenge

Return to module overview