Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose errors from the API endpoint in the UI #329

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
44 changes: 32 additions & 12 deletions Sample-01/api-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ const express = require("express");
const cors = require("cors");
const morgan = require("morgan");
const helmet = require("helmet");
const { auth } = require("express-oauth2-jwt-bearer");
const {
auth,
InvalidTokenError,
InvalidRequestError,
InsufficientScopeError
} = require("express-oauth2-jwt-bearer");
const authConfig = require("./src/auth_config.json");

const app = express();
Expand All @@ -11,11 +16,7 @@ const port = process.env.API_PORT || 3001;
const appPort = process.env.SERVER_PORT || 3000;
const appOrigin = authConfig.appOrigin || `http://localhost:${appPort}`;

if (
!authConfig.domain ||
!authConfig.audience ||
authConfig.audience === "YOUR_API_IDENTIFIER"
) {
if (!authConfig.domain || !authConfig.audience || authConfig.audience === "YOUR_API_IDENTIFIER") {
console.log(
"Exiting: Please make sure that auth_config.json is in place and populated with valid domain and audience values"
);
Expand All @@ -27,16 +28,35 @@ app.use(morgan("dev"));
app.use(helmet());
app.use(cors({ origin: appOrigin }));

const checkJwt = auth({
audience: authConfig.audience,
issuerBaseURL: `https://${authConfig.domain}/`,
algorithms: ["RS256"],
});
app.use(
auth({
audience: authConfig.audience,
issuerBaseURL: `https://${authConfig.domain}/`,
algorithms: ["RS256"],
})
);

app.get("/api/external", checkJwt, (req, res) => {
app.get("/api/external", (req, res) => {
res.send({
msg: "Your access token was successfully validated!",
});
});

// Custom error handler that will turn the errors from express-oauth2-jwt-bearer into a JSON object
// for the UI to handle
app.use((err, req, res, next) => {
if (
err instanceof InvalidTokenError ||
err instanceof InvalidRequestError ||
err instanceof InsufficientScopeError
) {
return res.status(err.status).send({
error: err.code,
message: err.message,
});
}

res.send(err);
});

app.listen(port, () => console.log(`API Server listening on port ${port}`));
92 changes: 21 additions & 71 deletions Sample-01/src/components/Highlight.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,23 @@
import React, { Component } from "react";
import PropTypes from "prop-types";

import hljs from "highlight.js";
import "highlight.js/styles/monokai-sublime.css";

const registeredLanguages = {};

class Highlight extends Component {
constructor(props) {
super(props);

this.state = { loaded: false };
this.codeNode = React.createRef();
}

componentDidMount() {
const { language } = this.props;

if (language && !registeredLanguages[language]) {
try {
const newLanguage = require(`highlight.js/lib/languages/${language}`);
hljs.registerLanguage(language, newLanguage);
registeredLanguages[language] = true;

this.setState({ loaded: true }, this.highlight);
} catch (e) {
console.error(e);
throw Error(`Cannot register the language ${language}`);
}
} else {
this.setState({ loaded: true });
}
}

componentDidUpdate() {
this.highlight();
}

highlight = () => {
this.codeNode &&
this.codeNode.current &&
hljs.highlightBlock(this.codeNode.current);
};

render() {
const { language, children } = this.props;
const { loaded } = this.state;

if (!loaded) {
return null;
}

return (
<pre className="rounded">
<code ref={this.codeNode} className={language}>
{children}
</code>
</pre>
);
}
}

Highlight.propTypes = {
children: PropTypes.node.isRequired,
language: PropTypes.string,
};

Highlight.defaultProps = {
language: "json",
import React, { useEffect, useRef } from 'react';

import hljs from 'highlight.js';
import 'highlight.js/styles/monokai-sublime.css';

const Highlight = (props) => {
const { text, language = 'json' } = props;

const codeNode = useRef(null);
useEffect(() => {
hljs.highlightElement(codeNode.current);
}, [text]);

return (
<pre className="rounded">
<code ref={codeNode} className={language}>
{text}
</code>
</pre>
);
};

export default Highlight;
export default Highlight;
30 changes: 26 additions & 4 deletions Sample-01/src/views/ExternalApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export const ExternalApiComponent = () => {

const [state, setState] = useState({
showResult: false,
apiMessage: "",
showApiError: false,
error: null,
apiMessage: "",
apiError: null,
});

const {
Expand Down Expand Up @@ -64,12 +66,27 @@ export const ExternalApiComponent = () => {
},
});

if (!response.ok) {
const apiError = response.headers.get('content-type')?.includes('application/json')
? JSON.stringify(await response.json(), null, 2)
: await response.text();
setState({
...state,
showApiError: true,
showResult: false,
apiError
});
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering if this isnt a bit too much for anyone using the sample to try and understand how to use our SDK. I know this isnt necessary related to our SDK, but not everyone might understand that.

In what event do we get back non-json?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I realise now this is probably too much for the sample, the only case that raw HTML would be returned is when the server errors with something other than a token error. So maybe in that instance we can just ignore?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would argue we could abstract this in a component? That way it's all at least seperated from basic integration?


const responseData = await response.json();

setState({
...state,
showResult: true,
apiMessage: responseData,
apiError: null,
showApiError: false
});
} catch (error) {
setState({
Expand Down Expand Up @@ -185,9 +202,14 @@ export const ExternalApiComponent = () => {
{state.showResult && (
<div className="result-block" data-testid="api-result">
<h6 className="muted">Result</h6>
<Highlight>
<span>{JSON.stringify(state.apiMessage, null, 2)}</span>
</Highlight>
<Highlight text={JSON.stringify(state.apiMessage, null, 2)} />
</div>
)}

{state.showApiError && (
<div className="result-block" data-testid="api-result">
<h6 className="muted">Error</h6>
<Highlight text={state.apiError} />
</div>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion Sample-01/src/views/Profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const ProfileComponent = () => {
</Col>
</Row>
<Row>
<Highlight>{JSON.stringify(user, null, 2)}</Highlight>
<Highlight text={JSON.stringify(user, null, 2)} />
</Row>
</Container>
);
Expand Down