Skip to content

Commit 2ca0f14

Browse files
cs-rajclaude
andcommitted
fix(bootstrap): reject on non-2xx tarball response and handle zlib errors
streamRelease now throws GithubError for HTTP 4xx/5xx responses instead of silently piping the error body (e.g. "404: Not Found") into the zlib decompressor. This was the root cause of the Z_DATA_ERROR crash when the cli-use branch was absent from a repo. extract now attaches an error handler directly on the zlib.createUnzip() stream. Node's pipe() does not forward stream errors, so without this listener a zlib failure emitted an unhandled error event and crashed the process rather than rejecting the Promise cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d1aaa7e commit 2ca0f14

3 files changed

Lines changed: 119 additions & 1 deletion

File tree

‎.talismanrc‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,6 @@ fileignoreconfig:
3939
checksum: a64a4d396eddd936a63b799eff58c5c6660b5dcaa3a310fd8b09a027932f1789
4040
- filename: packages/contentstack-migration/README.md
4141
checksum: e96006c1a948f766c88ae972b29582fa58eaf8184606bf011eebddc5a06cd7b6
42+
- filename: packages/contentstack-bootstrap/test/github.test.js
43+
checksum: b7badfcd3bbad0cb876364542bba26cdfd854f1b138be2896b5f84c219767040
4244
version: ""

‎packages/contentstack-bootstrap/src/bootstrap/github/client.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,23 @@ export default class GitHubClient {
7474
}
7575

7676
const response = await HttpClient.create().options(options).get(url);
77+
78+
if (response.status >= 400) {
79+
throw new GithubError(
80+
messageHandler.parse('CLI_BOOTSTRAP_REPO_NOT_FOUND', `${this.repo.user}/${this.repo.name}`),
81+
response.status,
82+
);
83+
}
84+
7785
return response.data as Stream;
7886
}
7987

8088
async extract(destination: string, stream: Stream): Promise<any> {
8189
return new Promise((resolve, reject) => {
90+
const unzip = zlib.createUnzip();
91+
unzip.on('error', reject);
8292
stream
83-
.pipe(zlib.createUnzip())
93+
.pipe(unzip)
8494
.pipe(
8595
tar.extract({
8696
cwd: destination,

‎packages/contentstack-bootstrap/test/github.test.js‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
const { expect } = require('chai');
2+
const sinon = require('sinon');
3+
const { Readable } = require('stream');
4+
const { HttpClient } = require('@contentstack/cli-utilities');
25
const GitHubClient = require('../lib/bootstrap/github/client').default;
6+
const GithubError = require('../lib/bootstrap/github/github-error').default;
37

48
describe('Github Client', function () {
59
it('Parse github url', () => {
@@ -16,4 +20,106 @@ describe('Github Client', function () {
1620
'https://api.github.com/repos/contentstack/contentstack-nextjs-react-universal-demo/tarball/cli-use',
1721
);
1822
});
23+
24+
describe('streamRelease', function () {
25+
let sandbox;
26+
27+
beforeEach(() => {
28+
sandbox = sinon.createSandbox();
29+
});
30+
31+
afterEach(() => {
32+
sandbox.restore();
33+
});
34+
35+
it('should throw GithubError with status 404 when the branch does not exist', async () => {
36+
const notFoundStream = new Readable({ read() {} });
37+
notFoundStream.push(Buffer.from('404: Not Found'));
38+
notFoundStream.push(null);
39+
40+
const httpStub = { get: sandbox.stub().resolves({ status: 404, data: notFoundStream }), options: sandbox.stub().returnsThis() };
41+
sandbox.stub(HttpClient, 'create').returns(httpStub);
42+
43+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next'));
44+
45+
try {
46+
await client.streamRelease(client.gitTarBallUrl);
47+
throw new Error('Expected GithubError to be thrown');
48+
} catch (err) {
49+
expect(err).to.be.instanceOf(GithubError);
50+
expect(err.status).to.equal(404);
51+
}
52+
});
53+
54+
it('should throw GithubError with status 500 on server error', async () => {
55+
const errStream = new Readable({ read() {} });
56+
errStream.push(Buffer.from('Internal Server Error'));
57+
errStream.push(null);
58+
59+
const httpStub = { get: sandbox.stub().resolves({ status: 500, data: errStream }), options: sandbox.stub().returnsThis() };
60+
sandbox.stub(HttpClient, 'create').returns(httpStub);
61+
62+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next'));
63+
64+
try {
65+
await client.streamRelease(client.gitTarBallUrl);
66+
throw new Error('Expected GithubError to be thrown');
67+
} catch (err) {
68+
expect(err).to.be.instanceOf(GithubError);
69+
expect(err.status).to.equal(500);
70+
}
71+
});
72+
73+
it('should return the response stream when status is 200', async () => {
74+
const mockStream = new Readable({ read() {} });
75+
const httpStub = { get: sandbox.stub().resolves({ status: 200, data: mockStream }), options: sandbox.stub().returnsThis() };
76+
sandbox.stub(HttpClient, 'create').returns(httpStub);
77+
78+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next'));
79+
const result = await client.streamRelease(client.gitTarBallUrl);
80+
81+
expect(result).to.equal(mockStream);
82+
});
83+
84+
it('should pass Authorization header for private repos', async () => {
85+
const mockStream = new Readable({ read() {} });
86+
const httpStub = { get: sandbox.stub().resolves({ status: 200, data: mockStream }), options: sandbox.stub().returnsThis() };
87+
sandbox.stub(HttpClient, 'create').returns(httpStub);
88+
89+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/private-repo'), true, 'my-token');
90+
await client.streamRelease(client.gitTarBallUrl);
91+
92+
const callOptions = httpStub.options.firstCall.args[0];
93+
expect(callOptions.headers).to.deep.equal({ Authorization: 'token my-token' });
94+
});
95+
96+
it('should throw GithubError immediately for private repos with no access token', async () => {
97+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/private-repo'), true, undefined);
98+
99+
try {
100+
await client.streamRelease(client.gitTarBallUrl);
101+
throw new Error('Expected GithubError to be thrown');
102+
} catch (err) {
103+
expect(err).to.be.instanceOf(GithubError);
104+
expect(err.status).to.equal(1);
105+
}
106+
});
107+
});
108+
109+
describe('extract', function () {
110+
it('should reject (not crash the process) when the stream contains invalid gzip data', async () => {
111+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next'));
112+
113+
const badStream = new Readable({ read() {} });
114+
badStream.push(Buffer.from('404: Not Found'));
115+
badStream.push(null);
116+
117+
try {
118+
await client.extract('/tmp', badStream);
119+
throw new Error('Expected extraction error to be thrown');
120+
} catch (err) {
121+
expect(err.code).to.equal('Z_DATA_ERROR');
122+
}
123+
});
124+
});
19125
});

0 commit comments

Comments
 (0)